Encapsulación para samurai real, o los matices asociados con la palabra clave interna en C #

Prólogo: interno es nuevo público


Cada uno de nosotros soñó con un proyecto donde todo se haría bien. Parece bastante natural. Tan pronto como se entere de la posibilidad de escribir un buen código, tan pronto como escuche leyendas sobre el mismo código que puede leerse y modificarse fácilmente, inmediatamente se ilumina: "Bueno, ahora lo haré bien, soy inteligente y leo McConnell".


imagen

Tal proyecto sucedió en mi vida. Otro. Y lo estoy haciendo bajo supervisión voluntaria, donde cada línea que sigo. En consecuencia, no solo quería hacerlo, sino que tenía que hacer todo bien. Uno de los "correctos" fue "respetar la encapsulación y acercarse al máximo, porque siempre tienes tiempo para abrir, y entonces será demasiado tarde para cerrar". Y por lo tanto, donde pude, comencé a usar el modificador de acceso interno en lugar de público para las clases. Y, por supuesto, cuando comienzas a utilizar activamente una nueva función de lenguaje para ti, surgen algunos matices. Quiero hablar sobre ellos en orden.


Ayuda básica ofensiva

Solo para recordar y etiquetar.


  • Assembly es la unidad de implementación más pequeña en .NET y una de las unidades de compilación básicas. Tal como está, esto es .dll o .exe. Dicen que se puede dividir en varios archivos llamados módulos.
  • modificador de acceso público, lo que significa que es accesible para todos los que estén marcados con él.
  • modificador de acceso interno, lo que significa que está marcado solo disponible dentro del ensamblaje.
  • protegido: un modificador de acceso que indica que está marcado solo disponible para los herederos de la clase en la que se encuentra el marcado.
  • privado: un modificador de acceso que indica que está marcado solo disponible para la clase en la que se encuentra. Y nadie mas.


Pruebas unitarias y compilaciones amigables


En C ++, había una característica tan extraña como las clases amistosas. Las clases se podían asignar como amigos, y luego se borraba el límite de encapsulación entre ellos. Sospecho que esta no es la característica más extraña en C ++. Quizás incluso los diez primeros más extraños no están incluidos. Pero pegarse un tiro en el pie al vincular varias clases estrechamente, de alguna manera es demasiado fácil, y es muy difícil encontrar un caso adecuado para esta función.


Lo más sorprendente fue aprender que en .NET hay ensamblajes amigables, una especie de replanteamiento. Es decir, puede hacer que un ensamblaje vea lo que está oculto detrás del bloqueo interno en otro ensamblaje. Cuando me enteré de esto, me sorprendió un poco. Bueno, ¿cómo lo haría, por qué? Cual es el punto? ¿Quién unirá firmemente a las dos asambleas, comprometidas en su separación? Casos en que en cualquier situación incomprensible moldean al público, no lo consideramos en este artículo.


Y luego, en el mismo proyecto, comencé a aprender una de las ramas del camino de un verdadero samurai: las pruebas unitarias. Y en las pruebas unitarias de Feng Shui deben realizarse en un ensamblaje separado. Para el mismo Feng Shui, todo lo que se puede ocultar dentro del ensamblaje, debe esconderse dentro del ensamblaje. Me enfrenté a una elección muy, muy desagradable. O las pruebas se colocarán una al lado de la otra y se dirigirán al cliente junto con el código útil para él, o todo estará cubierto por la palabra clave public, cuánto tiempo ha estado el pan en la humedad.


Y aquí, desde algún lugar en los contenedores de mi memoria, se obtuvo algo sobre asambleas amistosas. Resultó que si tienes el ensamblado "YourAssemblyName", puedes escribir así:


[assembly: InternalsVisibleTo("YourAssemblyName.Tests")] 

Y el ensamblado "YourAssemblyName.Tests" verá lo que está marcado con la palabra clave interna en "YourAssemblyName". Esta línea se puede ingresar, solo un poco, en AssemblyInfo.cs, que VS crea específicamente para almacenar dichos atributos.


Volver abusivo a la ayuda básica
En .NET, además de los atributos o palabras clave ya incorporados como abstracto, público, interno, estático, puede crear los suyos propios. Y cuélguelos en lo que desee: campos, propiedades, clases, métodos, eventos y ensamblajes completos. En C #, para esto simplemente escribe el nombre del atributo entre corchetes antes de lo que espera. La excepción es el ensamblaje en sí, ya que no hay ninguna indicación directa en ninguna parte del código de que "El ensamblaje comienza aquí". Allí, antes del nombre del atributo, debe agregar ensamblado:

Por lo tanto, los lobos permanecen llenos, las ovejas están a salvo, todo lo que es posible todavía se esconde dentro de la asamblea, las pruebas unitarias viven en una asamblea separada, como debería ser, y una característica que apenas recordaba tiene una razón para usarla. Quizás la única razón existente.


Casi olvido un punto importante. La acción de atributo InternalsVisibleTo es unidireccional.


protegido <interno?


Entonces la situación: A y B estaban sentados en una tubería.


 using System; namespace Pipe { public class A { public String SomeProperty { get; protected set; } } internal class B { //ERROR!!! The accessibility modifier of the 'B.OtherProperty.set' accessor must be more //restrictive than the property or indexer 'B.OtherProperty' internal String OtherProperty { get; protected set; } } } 

A fue destruido en el proceso de revisión del código, ya que no se usa fuera del ensamblado, pero por alguna razón se permite tener un modificador de acceso público, B causó un error de compilación, lo que podría provocar un estupor en los primeros minutos.


Básicamente, el mensaje de error es lógico. El descriptor de acceso a la propiedad no puede revelar más que la propiedad misma. Cualquiera reaccionará con comprensión si el compilador da un encabezado para esto:


 internal String OtherProperty { get; public set; } 

Pero las afirmaciones de esta línea rompen inmediatamente el cerebro:


 internal String OtherProperty { get; protected set; } 

Observo que no habrá quejas sobre esta línea:


 internal String OtherProperty { get; private set; } 

Si no piensa mucho, la siguiente jerarquía se construye en su cabeza:


 public > internal > protected > private 

Y esta jerarquía parece funcionar incluso. Excepto por un lugar. Donde interno> protegido. Para comprender la esencia de las afirmaciones del compilador, recordemos qué restricciones imponen los internos y los protegidos. interno: solo dentro del conjunto. protegido - solo herederos. Observe a los herederos. Y si la clase B está marcada como pública, en otro ensamblaje puede definir sus descendientes. Y luego el conjunto de acceso realmente obtiene acceso a donde toda la propiedad no lo tiene. Como el compilador de C # es paranoico, ni siquiera puede permitir tal posibilidad.


Gracias a él por esto, pero debemos darles a los herederos acceso al descriptor de acceso. Y específicamente para tales casos, hay un modificador de acceso interno protegido.


Esta ayuda no es tan ofensiva
  • interno protegido: un modificador de acceso que indica que el marcado está disponible dentro del ensamblaje o para los herederos de la clase en la que se encuentra el marcado.


Entonces, si queremos que el compilador nos permita usar esta propiedad y establecerla en los herederos, debemos hacer esto:


 using System; namespace Pipe { internal class B { protected internal String OtherProperty { get; protected set; } } } 

Y la jerarquía correcta de modificadores de acceso se ve así:


 public > protected internal > internal/protected > private 

Interfaces


Entonces, la situación: A, I, B estábamos sentados en la tubería.


 namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { internal void SomeMethod() { //'A' does not implement interface member 'I.SomeMethod()'. //'A.SomeMethod()' cannot implement an interface member because it is not public. } } internal class B : I { internal void SomeMethod() { //'B' does not implement interface member 'I.SomeMethod()'. //'B.SomeMethod()' cannot implement an interface member because it is not public. } } } 

Nos sentamos exactamente y no nos entrometimos fuera de la asamblea. Pero fueron rechazados por el compilador. Aquí la esencia de las afirmaciones queda clara en el mensaje de error. La implementación de la interfaz debe estar abierta. Incluso si la interfaz en sí está cerrada. Sería lógico vincular el acceso a la implementación de la interfaz con su disponibilidad, pero lo que no es, no lo es. La implementación de la interfaz debe ser pública.


Y tenemos dos salidas. Primero: a través del crujido y el crujir de dientes, cuelgue un modificador de acceso público en la implementación de la interfaz. Segundo: implementación explícita de la interfaz. Se ve así:


 namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { public void SomeMethod() { } } internal class B : I { void I.SomeMethod() { } } } 

Tenga en cuenta que en el segundo caso no hay modificador de acceso. ¿Para quién en este caso está disponible la implementación del método? Digamos que nadie. Es más fácil mostrar con un ejemplo:


 B b = new B(); //'B' does not contain a definition for 'SomeMethod' and no accessible extension method //'SomeMethod' accepting a first argument of type 'B' could be found //(are you missing a using directive or an assembly reference?) b.SomeMethod(); //OK (b as I).SomeMethod(); 

La implementación explícita de la interfaz I significa que hasta que expulsamos explícitamente la variable al tipo I, no hay métodos que implementen esta interfaz. Escribir (b como I) .SomeMethod () cada vez puede ser una sobrecarga. Como ((I) b) .SomeMethod (). Y encontré dos formas de evitar esto. Pensé en uno, y honestamente busqué en Google el segundo.


La primera forma es la fábrica:


  internal class Factory { internal I Create() { return new B(); } } 

Bueno, o cualquier otro patrón que te permita ocultar este matiz.


Método dos - métodos de extensión:


  internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } } 

Sorprendentemente, funciona. Estas líneas dejan de arrojar un error:


 B b = new B(); b.SomeMethod(); 

Después de todo, la llamada llega, como IntelliSense nos dice en Visual Studio, no a los métodos para implementar explícitamente la interfaz, sino a los métodos de extensión. Y nadie prohíbe recurrir a ellos. Y pueden llamarse métodos de extensión de interfaz en todas sus implementaciones.


Pero queda una advertencia. Dentro de la propia clase, debe acceder a este método a través de la palabra clave this; de lo contrario, el compilador no comprenderá que queremos hacer referencia al método de extensión:


  internal class B : I { internal void OtherMethod() { //Error!!! SomeMethod(); //OK this.SomeMethod(); } void I.SomeMethod() { } } 

Y así, y así, tenemos o público, donde no debería estar, pero allí parece no hacer daño, o un pequeño código adicional para cada interfaz interna. Elige el mal menor a tu gusto.


Reflexion


Golpeé esto dolorosamente cuando intenté encontrar un constructor a través de la reflexión, que, por supuesto, estaba marcado como interno en la clase interna. Y resultó que la reflexión no dará nada que no sea público. Y esto, en principio, es lógico.


En primer lugar, reflexión, si recuerdo correctamente lo que escribieron las personas inteligentes en los libros inteligentes, se trata de encontrar información en los metadatos del ensamblado. Lo cual, en teoría, no debería ceder demasiado (al menos eso pensé). En segundo lugar, el uso principal de la reflexión es hacer que su programa sea extensible. Proporcionas algún tipo de interfaz a los extraños (tal vez incluso en forma de interfaces, ¡fiy-ha!). Y lo implementan y proporcionan complementos, modificaciones, extensiones en forma de un ensamblaje cargado sobre la marcha, desde el cual los obtiene la reflexión. Y por sí mismo, su API será pública. Es decir, mirar internamente a través de la reflexión no es técnicamente ni tiene sentido desde un punto de vista práctico.


Actualización Aquí, en los comentarios, resultó que la reflexión permite, si lo solicita explícitamente, reflejar todo. Ya sea interno, incluso privado. Si no está escribiendo algún tipo de herramienta de análisis de código, intente no hacerlo, por favor. El texto a continuación sigue siendo relevante para los casos en los que estamos buscando tipos de miembros abiertos. Y en general, no pase comentarios, hay muchas cosas interesantes.


Esto podría terminar con la reflexión, pero volvamos al ejemplo anterior, donde A, I, B estaban sentados en una tubería:


 namespace Pipe { internal interface I { void SomeMethod(); } internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } } internal class A : I { public void SomeMethod() { } internal void OtherMethod() { } } internal class B : I { internal void OtherMethod() { } void I.SomeMethod() { } } } 

El autor de la clase A decidió que no pasaría nada malo si el método de la clase interna se marcara como público, de modo que el compilador no doliera y no hubiera necesidad de poner más código en él. La interfaz está marcada como interna, la clase que la implementa está marcada como interna, desde el exterior parece que no hay forma de llegar al método marcado como público.


Y luego la puerta se abre y la reflexión se arrastra silenciosamente:


 using Pipe; using System; using System.Reflection; namespace EncapsulationTest { public class Program { public static void Main(string[] args) { FindThroughReflection(typeof(I), "SomeMethod"); FindThroughReflection(typeof(IExtensions), "SomeMethod"); FindThroughReflection(typeof(A), "SomeMethod"); FindThroughReflection(typeof(A), "OtherMethod"); FindThroughReflection(typeof(B), "SomeMethod"); FindThroughReflection(typeof(B), "OtherMethod"); Console.ReadLine(); } private static void FindThroughReflection(Type type, String methodName) { MethodInfo methodInfo = type.GetMethod(methodName); if (methodInfo != null) Console.WriteLine($"In type {type.Name} we found {methodInfo}"); else Console.WriteLine($"NULL! Can't find method {methodName} in type {type.Name}"); } } } 

Estudie este código, llévelo al estudio, si así lo desea. Aquí estamos tratando de usar la reflexión para encontrar todos los métodos de todos los tipos de nuestra tubería (tubería de espacio de nombres). Y aquí están los resultados que nos da:


En el tipo I encontramos Void SomeMethod ()
NULL! No se puede encontrar el método SomeMethod en el tipo IExtensions
En el tipo A encontramos Void SomeMethod ()
NULL! No se puede encontrar el método OtherMethod en el tipo A
NULL! No se puede encontrar el método SomeMethod en el tipo B
NULL! No se puede encontrar el método OtherMethod en el tipo B

Debo decir de inmediato que usando un objeto de tipo MethodInfo, se puede llamar al método encontrado. Es decir, si la reflexión encontró algo, entonces la encapsulación puede violarse puramente teóricamente. Y hemos encontrado algo. En primer lugar, el mismo vacío público SomeMethod () de la clase A. Se esperaba, qué más decir. Esta indulgencia aún puede tener consecuencias. En segundo lugar, anule SomeMethod () de la interfaz I. Esto ya es más interesante. No importa cómo nos encerramos, los métodos abstractos ubicados en la interfaz (o lo que el CLR realmente coloca allí) están realmente abiertos. De ahí la conclusión hecha en un párrafo separado:


Mire cuidadosamente a quién y qué tipo de sistema. Tipo de tipo que está regalando.


Pero hay un matiz más con estos dos métodos encontrados, que me gustaría considerar. Los métodos de interfaz interna y los métodos públicos de clases internas se pueden encontrar utilizando la reflexión. Como persona razonable, concluiré que caen en los metadatos. Como persona experimentada, verificaré esta conclusión. Y en este ILDasm nos ayudará.


Echa un vistazo al agujero del conejo en los metadatos de nuestra pipa

El ensamblaje se ensambló en versión


TypeDef #2 (02000003)
-------------------------------------------------------
TypDefName: Pipe.I (02000003)
Flags : [NotPublic] [AutoLayout] [Interface] [Abstract] [AnsiClass] (000000a0)
Extends : 01000000 [TypeRef]
Method #1 (06000004)
-------------------------------------------------------
MethodName: SomeMethod (06000004)
Flags : [Public] [Virtual] [HideBySig] [NewSlot] [Abstract] (000005c6)
RVA : 0x00000000
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

TypeDef #3 (02000004)
-------------------------------------------------------
TypDefName: Pipe.IExtensions (02000004)
Flags : [NotPublic] [AutoLayout] [Class] [Abstract] [Sealed] [AnsiClass] [BeforeFieldInit] (00100180)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000005)
-------------------------------------------------------
MethodName: SomeMethod (06000005)
Flags : [Assem] [Static] [HideBySig] [ReuseSlot] (00000093)
RVA : 0x00002134
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
1 Arguments
Argument #1: Class Pipe.I
1 Parameters
(1) ParamToken : (08000004) Name : i flags: [none] (00000000)
CustomAttribute #1 (0c000011)
-------------------------------------------------------
CustomAttribute Type: 0a000001
CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
Length: 4
Value : 01 00 00 00 > <
ctor args: ()

CustomAttribute #1 (0c000010)
-------------------------------------------------------
CustomAttribute Type: 0a000001
CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
Length: 4
Value : 01 00 00 00 > <
ctor args: ()

TypeDef #4 (02000005)
-------------------------------------------------------
TypDefName: Pipe.A (02000005)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000006)
-------------------------------------------------------
MethodName: SomeMethod (06000006)
Flags : [Public] [Final] [Virtual] [HideBySig] [NewSlot] (000001e6)
RVA : 0x0000213c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #2 (06000007)
-------------------------------------------------------
MethodName: OtherMethod (06000007)
Flags : [Assem] [HideBySig] [ReuseSlot] (00000083)
RVA : 0x0000213e
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #3 (06000008)
-------------------------------------------------------
MethodName: .ctor (06000008)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
RVA : 0x00002140
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

InterfaceImpl #1 (09000001)
-------------------------------------------------------
Class : Pipe.A
Token : 02000003 [TypeDef] Pipe.I

TypeDef #5 (02000006)
-------------------------------------------------------
TypDefName: Pipe.B (02000006)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000009)
-------------------------------------------------------
MethodName: OtherMethod (06000009)
Flags : [Assem] [HideBySig] [ReuseSlot] (00000083)
RVA : 0x00002148
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #2 (0600000a)
-------------------------------------------------------
MethodName: Pipe.I.SomeMethod (0600000A)
Flags : [Private] [Final] [Virtual] [HideBySig] [NewSlot] (000001e1)
RVA : 0x0000214a
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #3 (0600000b)
-------------------------------------------------------
MethodName: .ctor (0600000B)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
RVA : 0x0000214c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

MethodImpl #1 (00000001)
-------------------------------------------------------
Method Body Token : 0x0600000a
Method Declaration Token : 0x06000004

InterfaceImpl #1 (09000002)
-------------------------------------------------------
Class : Pipe.B
Token : 02000003 [TypeDef] Pipe.I


Una mirada rápida muestra que todo entra en los metadatos, sin importar cómo esté marcado. La reflexión aún nos oculta cuidadosamente que se supone que los extraños no deben ver. Por lo tanto, puede ser que las cinco líneas de código adicionales para cada método de la interfaz interna no sean un gran mal. Sin embargo, la conclusión principal sigue siendo la misma:


Mire cuidadosamente a quién y qué tipo de sistema. Tipo de tipo que está regalando.


Pero este es, por supuesto, el siguiente nivel, después de la adhesión de la palabra clave interna en todos los lugares donde no hay necesidad de público.


PS


¿Sabes que lo mejor de usar la palabra clave interna está en todas partes dentro del ensamblaje? Cuando crece, debes dividirlo en dos o más. Y en el proceso, debes tomar un descanso para abrir algunos tipos. Y tiene que pensar exactamente qué tipos son dignos de abrirse. Al menos brevemente.


Esto significa lo siguiente: esta práctica de escribir código lo hará pensar nuevamente sobre qué forma tomará el límite arquitectónico entre los conjuntos recién nacidos. ¿Qué podría ser más hermoso?


PPS


A partir de la versión C # 7.2, apareció un nuevo modificador de acceso, privado protegido. Y todavía no tengo idea de qué es y con qué se come. Desde que no se encuentra en la práctica. Pero me alegrará saber en los comentarios. Pero no copie y pegue de la documentación, sino casos reales en los que este modificador de acceso puede ser necesario.

Source: https://habr.com/ru/post/445804/


All Articles