Prólogo: interno é novo público
Cada um de nós sonhava com um projeto onde tudo seria feito corretamente. Parece bastante natural. Assim que você aprende sobre a possibilidade de escrever um bom código, assim que ouve lendas sobre o mesmo código que pode ser facilmente lido e modificado, você se ilumina imediatamente: "Bem, agora eu vou fazer isso direito, sou inteligente e leio McConnell".

Esse projeto aconteceu na minha vida. Outro. E estou fazendo isso sob supervisão voluntária, onde cada linha que eu sigo. Consequentemente, não apenas eu queria, mas também precisava fazer tudo certo. Um dos "direitos" era "respeitar o encapsulamento e quase o máximo, porque você sempre tem tempo para abrir e será tarde demais para fechar". E, portanto, onde quer que eu pudesse, comecei a usar o modificador de acesso interno em vez de público para as aulas. E, é claro, quando você começa a usar ativamente um novo recurso de idioma para você, surgem algumas nuances. Eu quero falar sobre eles em ordem.
Ajuda básica ofensivaApenas para lembrar e rotular.
- Assembly é a menor unidade de implantação no .NET e uma das unidades básicas de compilação. Como é, isso é .dll ou .exe. Eles dizem que pode ser dividido em vários arquivos chamados módulos.
- modificador de acesso público, o que significa que é acessível a todos marcados com ele.
- modificador de acesso interno, o que significa que ele está marcado apenas disponível dentro da montagem.
- protected - um modificador de acesso que indica que está marcado apenas disponível para os herdeiros da classe em que o marcado está localizado.
- private - um modificador de acesso que indica que está marcado apenas disponível para a classe em que está localizado. E mais ninguém.
Testes de unidade e construções amigáveis
No C ++, havia um recurso tão estranho quanto as classes amigáveis. As turmas podiam ser designadas como amigas e, em seguida, a fronteira do encapsulamento entre elas era apagada. Eu suspeito que esse não seja o recurso mais estranho em C ++. Talvez até os dez mais estranhos não estejam incluídos. Mas dar um tiro no pé vinculando várias classes com força é algo fácil demais e é muito difícil encontrar um caso adequado para esse recurso.
O mais surpreendente foi saber que no .NET existem assembléias amigáveis, uma espécie de repensar. Ou seja, você pode fazer uma montagem ver o que está oculto atrás da trava interna em outra montagem. Quando descobri isso, fiquei um pouco surpreso. Bem, como seria, por quê? Qual é o objetivo? Quem amarrará firmemente as duas assembléias, envolvidas em sua separação? Casos em que, em qualquer situação incompreensível, eles moldam o público, não consideramos neste artigo.
E então, no mesmo projeto, comecei a aprender um dos ramos do caminho de um samurai real: teste de unidade. E no Feng Shui, os testes de unidade devem ser realizados em uma montagem separada. Para o mesmo Feng Shui, tudo o que pode ser escondido dentro da montagem, você precisa se esconder dentro da montagem. Enfrentei uma escolha muito, muito desagradável. Os testes ficarão lado a lado e vão para o cliente junto com o código útil para ele, ou tudo será coberto pela palavra-chave public, há quanto tempo o pão permanece na umidade.
E aqui, de algum lugar nas caixas da minha memória, algo foi obtido sobre assembléias amigáveis. Aconteceu que, se você tiver a montagem "YourAssemblyName", poderá escrever assim:
[assembly: InternalsVisibleTo("YourAssemblyName.Tests")]
E o assembly "YourAssemblyName.Tests" verá o que está marcado com a palavra-chave interna em "YourAssemblyName". Essa linha pode ser inserida, apenas um pouco, em AssemblyInfo.cs, que o VS cria especificamente para armazenar esses atributos.
Retornar abusivo à ajuda básicaNo .NET, além de atributos ou palavras-chave já incorporados, como abstrato, público, interno, estático, você pode criar seus próprios. E pendure-os no que quiser: campos, propriedades, classes, métodos, eventos e montagens inteiras. Em C #, para isso, basta escrever o nome do atributo entre colchetes antes do que você espera. A exceção é o próprio assembly, pois não há indicação direta em nenhum lugar do código que "Assembly começa aqui". Lá, antes do nome do atributo, você precisa adicionar o assembly:
Assim, os lobos permanecem cheios, as ovelhas estão seguras, tudo o que é possível ainda está escondido dentro da assembléia, os testes de unidade vivem em uma assembléia separada, como deveria ser, e um recurso que eu mal lembrava tem um motivo para usá-lo. Talvez a única razão existente.
Eu quase esqueci um ponto importante. A ação de atributo InternalsVisibleTo é unidirecional.
protegido <interno?
Então a situação: A e B estavam sentados em um cano.
using System; namespace Pipe { public class A { public String SomeProperty { get; protected set; } } internal class B {
A foi destruído no processo de revisão de código, uma vez que não é usado fora do assembly, mas por algum motivo se permite ter um modificador de acesso público, B causou um erro de compilação, o que poderia levar a um estupor nos primeiros minutos.
Basicamente, a mensagem de erro é lógica. O acessador de propriedade não pode revelar mais do que a própria propriedade. Qualquer pessoa reagirá com compreensão se o compilador fornecer um cabeçalho para isso:
internal String OtherProperty { get; public set; }
Mas reivindicações a essa linha quebram imediatamente o cérebro:
internal String OtherProperty { get; protected set; }
Observo que não haverá queixas sobre esta linha:
internal String OtherProperty { get; private set; }
Se você não pensa muito, a seguinte hierarquia é criada em sua cabeça:
public > internal > protected > private
E essa hierarquia parece funcionar. Exceto por um lugar. Onde interno> protegido. Para entender a essência das declarações do compilador, lembre-se de quais restrições são impostas por interno e protegido. interno - somente dentro da montagem. protegido - apenas herdeiros. Observe qualquer herdeiro. E se a classe B estiver marcada como pública, em outra montagem você poderá definir seus descendentes. E então o acessador definido realmente obtém acesso a onde toda a propriedade não a possui. Como o compilador C # é paranóico, ele não pode sequer permitir essa possibilidade.
Obrigado a ele por isso, mas precisamos dar aos herdeiros acesso ao assessor. E especificamente para esses casos, há um modificador de acesso interno protegido.
Essa ajuda não é tão ofensiva- interno protegido - um modificador de acesso que indica que o marcado está disponível dentro da montagem ou para os herdeiros da classe em que o marcado está localizado.
Portanto, se queremos que o compilador nos permita usar essa propriedade e configurá-la nos herdeiros, precisamos fazer o seguinte:
using System; namespace Pipe { internal class B { protected internal String OtherProperty { get; protected set; } } }
E a hierarquia correta dos modificadores de acesso se parece com isso:
public > protected internal > internal/protected > private
Interfaces
Então, a situação: A, eu, B estávamos sentados no cano.
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { internal void SomeMethod() {
Sentamos exatamente e não nos metemos do lado de fora da assembléia. Mas eles foram rejeitados pelo compilador. Aqui a essência das reivindicações é clara na mensagem de erro. A implementação da interface deve estar aberta. Mesmo se a própria interface estiver fechada. Seria lógico vincular o acesso à implementação da interface à sua disponibilidade, mas o que não é, não é. A implementação da interface deve ser pública.
E nós temos duas maneiras de sair. Primeiro: através do rangido e ranger de dentes, pendure um modificador de acesso público na implementação da interface. Segundo: implementação explícita da interface. É assim:
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { public void SomeMethod() { } } internal class B : I { void I.SomeMethod() { } } }
Observe que, no segundo caso, não há modificador de acesso. Para quem, neste caso, está disponível a implementação do método? Vamos apenas dizer ninguém. É mais fácil mostrar com um exemplo:
B b = new B();
A implementação explícita da interface I significa que, até lançarmos explicitamente a variável para o tipo I, não há métodos que implementem essa interface. Escrever (b como I) .SomeMethod () toda vez pode ser uma sobrecarga. Como (I) b) .AlgumMétodo (). E eu encontrei duas maneiras de contornar isso. Eu mesmo pensei em um e pesquisei honestamente no segundo.
A primeira maneira é a fábrica:
internal class Factory { internal I Create() { return new B(); } }
Bem, ou qualquer outro padrão que permita ocultar essa nuance.
Método dois - métodos de extensão:
internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } }
Surpreendentemente, funciona. Estas linhas param de gerar um erro:
B b = new B(); b.SomeMethod();
Afinal, a chamada chega, como o IntelliSense nos diz no Visual Studio, não para métodos para implementar explicitamente a interface, mas para métodos de extensão. E ninguém proíbe recorrer a eles. E os métodos de extensão de interface podem ser chamados em todas as suas implementações.
Mas resta uma ressalva. Dentro da própria classe, você precisa acessar esse método através da palavra-chave this; caso contrário, o compilador não entenderá que queremos nos referir ao método de extensão:
internal class B : I { internal void OtherMethod() {
E assim, e assim, temos ou público, onde não deveria estar, mas parece que não faz mal, ou um pouco de código extra para cada interface interna. Escolha o mal menor ao seu gosto.
Reflexão
Eu bati isso dolorosamente quando tentei encontrar um construtor através da reflexão, que, é claro, foi marcada como interna na classe interna. E aconteceu que a reflexão não divulgaria nada que não fosse público. E isso, em princípio, é lógico.
Em primeiro lugar, reflexão, se me lembro corretamente do que as pessoas inteligentes escreveram nos livros inteligentes, trata-se de encontrar informações nos metadados da montagem. O que, em teoria, não deve dar muito (pelo menos eu pensava). Em segundo lugar, o principal uso da reflexão é tornar seu programa extensível. Você fornece algum tipo de interface para pessoas de fora (talvez até na forma de interfaces, fiy-ha!). E eles o implementam e fornecem plugins, mods, extensões na forma de uma montagem carregada em movimento, a partir da qual a reflexão os obtém. E por si só, sua API será pública. Ou seja, olhar para a reflexão interna não é tecnicamente e sem sentido de um ponto de vista prático.
Update Aqui, nos comentários, descobriu-se que a reflexão permite, se você pedir explicitamente, refletir tudo. Seja interno, privado. Se você não está escrevendo algum tipo de ferramenta de análise de código, tente não fazer isso, por favor. O texto abaixo ainda é relevante para os casos em que estamos procurando tipos de membros abertos. E, em geral, não passe comentários, há muitas coisas interessantes.
Isso pode ser concluído com reflexão, mas vamos retornar ao exemplo anterior, onde A, I, B estavam sentados em um cano:
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() { } } }
O autor da classe A decidiu que nada de ruim aconteceria se o método da classe interna fosse marcado como público, para que o compilador não doesse e para que não houvesse necessidade de colocar mais código nele. A interface é marcada como interna, a classe que a implementa é marcada como interna; de fora, parece que não há como acessar o método marcado como público.
E então a porta se abre e o reflexo entra 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}"); } } }
Estude este código, leve-o para o estúdio, se desejar. Aqui, estamos tentando usar a reflexão para encontrar todos os métodos de todos os tipos de nosso pipe (namespace Pipe). E aqui estão os resultados que ele nos fornece:
No tipo I, encontramos Void SomeMethod ()
NULL! Não é possível encontrar o método SomeMethod no tipo IExtensions
No tipo A, encontramos Void SomeMethod ()
NULL! Não é possível encontrar o método OtherMethod no tipo A
NULL! Não é possível encontrar o método SomeMethod no tipo B
NULL! Não é possível encontrar o método OtherMethod no tipo B
Devo dizer imediatamente que, usando um objeto do tipo MethodInfo, o método encontrado pode ser chamado. Ou seja, se a reflexão encontrou algo, o encapsulamento pode ser violado puramente teoricamente. E nós encontramos algo. Em primeiro lugar, o mesmo público anula SomeMethod () da classe A. Era esperado, o que mais dizer. Essa indulgência ainda pode ter consequências. Em segundo lugar, anule SomeMethod () da interface I. Isso já é mais interessante. Não importa como nos trancemos, os métodos abstratos colocados na interface (ou o que o CLR realmente coloca lá) estão realmente abertos. Daí a conclusão feita em um parágrafo separado:
Observe com cuidado quem e que tipo de System.Type você está dando.
Mas há mais uma nuance com esses dois métodos encontrados, que eu gostaria de considerar. Métodos de interface interna e métodos públicos de classes internas podem ser encontrados usando reflexão. Como uma pessoa razoável, concluirei que eles se enquadram nos metadados. Como pessoa experiente, vou verificar esta conclusão. E neste ILDasm nos ajudará.
Dê uma olhada na toca do coelho nos metadados do nosso cachimboMontagem foi montada em Release
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
Uma rápida olhada mostra que tudo entra nos metadados, não importa como estejam marcados. A reflexão ainda cuidadosamente esconde de nós o que não se deve ver de fora. Portanto, pode ser que as cinco linhas de código extras para cada método da interface interna não sejam um grande mal. No entanto, a principal conclusão permanece a mesma:
Observe com cuidado quem e que tipo de System.Type você está dando.
Mas este é, obviamente, o próximo nível, após a adesão da palavra-chave interna em todos os lugares onde não há necessidade de público.
PS
Você sabe que a coisa mais legal sobre o uso da palavra-chave interna está em todo lugar na montagem? Quando cresce, você precisa dividi-lo em dois ou mais. E no processo, você precisa fazer uma pausa para abrir alguns tipos. E você precisa pensar exatamente sobre quais tipos merecem ser abertos. Pelo menos brevemente.
Isso significa o seguinte: essa prática de escrever código fará você pensar novamente sobre o formato da fronteira arquitetônica entre as assembléias recém-nascidas. O que poderia ser mais bonito?
PPS
Começando com a versão C # 7.2, um novo modificador de acesso, protegido privado, apareceu. E ainda não tenho ideia do que é e do que é comido. Desde que não encontrado na prática. Mas ficarei feliz em saber nos comentários. Mas não copie e cole da documentação, mas casos reais em que esse modificador de acesso pode ser necessário.