序幕:内部是新公共
我们每个人都梦想着一个可以正确完成所有事情的项目。 看起来很自然。 一旦您了解编写好的代码的可能性,一听到有关易于阅读和修改的代码的传说,您就会立即感到高兴:“好吧,现在我会做对的,我很聪明,我读了McConnell。”

这样的项目发生在我的生活中。 另一个 而且我在自愿监督下做这件事,我遵循的每一行。 因此,不仅我想做,而且我必须做正确的一切。 “正确的权利”之一是“尊重封装并尽可能地接近最大值,因为您总是有时间打开,然后关闭它为时已晚。” 因此,无论如何,我都开始在类内部使用access修饰符而不是public。 而且,当然,当您开始主动为您使用新的语言功能时,会出现一些细微差别。 我想按顺序谈论它们。
进攻性基本帮助仅提醒和标记。
- 程序集是.NET中最小的部署单元,也是基本的编译单元之一。 实际上,它是.dll或.exe。 他们说它可以分为几个称为模块的文件。
- public-访问修饰符,这意味着标记有该修饰符的所有人都可以使用它。
- internal-访问修饰符,这意味着仅将其标记为在装配件内部可用。
- protected-一个访问修饰符,指示该标记仅对标记所在的类的继承人可用。
- 私有-一个访问修饰符,指示它仅被标记为可用于其所在的类。 没有其他人。
单元测试和友好的构建
在C ++中,有一个奇怪的功能,如友好的类。 可以将类分配为好友,然后消除它们之间封装的边界。 我怀疑这不是C ++中最奇怪的功能。 也许甚至不包括前十名中最奇怪的人。 但是,通过紧密地联系多个班级来射击自己是一件容易的事,并且很难为此功能找到合适的情况。
更令人惊讶的是,发现.NET中存在友好的程序集,这是一种重新思考。 也就是说,您可以使一个程序集看到另一程序集中的内部锁后面隐藏的内容。 当我发现这一点时,我有些惊讶。 好吧,为什么呢? 有什么意义? 谁将两个装订分开的装订紧密地绑在一起? 在任何无法理解的情况下使公众受挫的情况,我们不在本文中考虑。
然后在同一个项目中,我开始学习真正武士的分支之一:单元测试。 而且在风水单元测试中应单独组装。 对于相同的风水,所有可以隐藏在部件中的东西,都需要隐藏在部件中。 我面临一个非常非常不愉快的选择。 测试要么并排进行,然后将对客户有用的代码与客户一起使用,要么所有内容都将被关键字public覆盖,即面包在潮湿环境中放置了多长时间。
在这里,从我记忆中的某个地方,获得了一些关于友好装配的信息。 原来,如果您具有程序集“ YourAssemblyName”,则可以这样编写:
[assembly: InternalsVisibleTo("YourAssemblyName.Tests")]
程序集“ YourAssemblyName.Tests”将看到在“ YourAssemblyName”中使用内部关键字标记的内容。 可以在VS专为存储此类属性而创建的AssemblyInfo.cs中输入这一行。
返回滥用基本帮助在.NET中,除了已经内置的属性或关键字(例如抽象,公共,内部,静态)之外,您还可以创建自己的属性或关键字。 并将它们挂在所需的任何内容上:字段,属性,类,方法,事件和整个程序集。 在C#中,为此,您只需在挂起之前在方括号中写上属性名称。 程序集本身是一个例外,因为在代码的任何地方都没有直接指示“程序集从这里开始”。 在那里,在属性名称之前,您需要添加程序集:
因此,狼保持饱满,绵羊是安全的,可能的一切仍然隐藏在程序集中,单元测试按应有的方式存在于单独的程序集中,而我几乎不记得的功能为使用它提供了理由。 也许是唯一存在的原因。
我几乎忘记了一个重点。 属性操作InternalsVisibleTo是单向的。
受保护的<内部?
这样的情况:A和B都坐在管道上。
using System; namespace Pipe { public class A { public String SomeProperty { get; protected set; } } internal class B {
A在代码检查过程中被破坏,因为它没有在程序集外部使用,但由于某种原因允许其自身具有公共访问修饰符,B导致了编译错误,这可能会在最初的几分钟内导致木僵。
基本上,错误消息是合乎逻辑的。 财产访问者不能透露比财产本身更多的东西。 如果编译器为此提供了标头,任何人都会做出反应:
internal String OtherProperty { get; public set; }
但是声称这条线立即打断了大脑:
internal String OtherProperty { get; protected set; }
我注意到,不会有关于此行的投诉:
internal String OtherProperty { get; private set; }
如果您不怎么想,就会在您的脑海中建立以下层次结构:
public > internal > protected > private
而且这种层次结构似乎甚至有效。 除了一个地方。 内部>受保护的地方。 为了理解编译器主张的实质,让我们回顾一下内部和受保护者施加了哪些限制。 内部-仅在装配内部。 受保护的-只有继承人。 注意任何继承人。 并且如果将B类标记为public,则可以在另一个程序集中定义其后代。 然后,set访问器实际上可以访问整个属性所没有的地方。 由于C#编译器是偏执狂,因此甚至不允许这种可能性。
感谢他的帮助,但是我们需要让继承人访问访问器。 特别是对于这种情况,有一个受保护的内部访问修饰符。
这种帮助并不那么令人反感- 受保护的内部-一个访问修饰符,它指示标记的一个在程序集内部或标记的所在类的继承人中可用。
因此,如果我们希望编译器允许我们使用此属性并将其设置在继承人中,则需要执行以下操作:
using System; namespace Pipe { internal class B { protected internal String OtherProperty { get; protected set; } } }
正确的访问修饰符层次结构如下所示:
public > protected internal > internal/protected > private
介面
因此,情况:A,I,B都坐在管道上。
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { internal void SomeMethod() {
我们正好坐在那里,没有干预大会。 但是它们被编译器拒绝了。 从错误消息中可以清楚地看到要求保护的实质。 接口的实现必须是开放的。 即使接口本身是关闭的。 将对接口实现的访问与接口的可用性联系在一起是合乎逻辑的,但不是,不是。 接口的实现必须是公共的。
我们有两种出路。 第一:通过吱吱作响和咬牙切齿,在接口的实现上挂一个公共访问修饰符。 第二:接口的显式实现。 看起来像这样:
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { public void SomeMethod() { } } internal class B : I { void I.SomeMethod() { } } }
请注意,在第二种情况下,没有访问修饰符。 在这种情况下,向谁提供方法的实现? 我们只说没人。 通过示例更容易显示:
B b = new B();
接口I的显式实现意味着在我们将变量显式转换为类型I之前,没有实现该接口的方法。 每次写(b as I).SomeMethod()可能是重载。 像((I)b).SomeMethod()。 我发现了两种解决方法。 我想到了一个自己,老实说,谷歌搜索了第二个。
第一种方法是工厂:
internal class Factory { internal I Create() { return new B(); } }
好吧,或者任何其他允许您隐藏此细微差别的模式。
方法二-扩展方法:
internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } }
令人惊讶的是,它有效。 这些行停止抛出错误:
B b = new B(); b.SomeMethod();
毕竟,正如IntelliSense在Visual Studio中告诉我们的那样,调用不是针对显式实现接口的方法,而是扩展方法。 而且没有人禁止转向他们。 接口扩展方法可以在其所有实现上调用。
但是仍然有一个警告。 在类本身内部,您需要通过this关键字访问此方法,否则编译器将无法理解我们要引用扩展方法:
internal class B : I { internal void OtherMethod() {
如此等等,我们拥有或公开了它不应该存在的地方,但是似乎没有害处,或者为每个内部接口添加了一些额外的代码。 选择自己喜欢的小邪恶。
倒影
当我尝试通过反射找到一个构造函数时,我感到很痛苦,该反射函数在内部类中被标记为内部构造。 事实证明,反思不会给出任何不公开的东西。 原则上,这是合乎逻辑的。
首先,反思,如果我没记错的话,聪明人在聪明书中写了什么,那是关于在程序集元数据中查找信息。 从理论上讲,这不应给出太多(至少我是这么认为的)。 其次,反射的主要用途是使程序可扩展。 您为外部人员提供了某种接口(甚至可能以接口的形式提供,fiy-ha!)。 他们实现了它,并提供了在旅途中加载的程序集形式的插件,mod,扩展,从中可以得到反射。 而且,您的API本身将是公开的。 也就是说,从实践的角度来看,通过反射观察内部并不是技术上毫无意义的。
更新资料 在这里,在评论中,事实证明,如果您明确提出要求,反思可以反映一切。 甚至是内部的,甚至是私有的。 如果您不编写某种代码分析工具,请不要这样做。 下面的文本对于我们正在寻找开放成员类型的情况仍然有用。 通常,不要通过评论,有很多有趣的事情。
这可以通过反射来完成,但是让我们回到前面的示例,其中A,I,B坐在管道上:
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() { } } }
类A的作者认为,如果将内部类的方法标记为public,则不会发生任何不好的情况,这样编译器就不会感到麻烦,从而无需在其中添加更多代码。 接口被标记为内部,实现该接口的类被标记为内部,从外部看,似乎没有办法获取标记为public的方法。
然后,门打开,反射悄悄地蔓延:
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}"); } } }
如果愿意,请研究此代码,将其驱动到工作室中。 在这里,我们尝试使用反射从管道的所有类型(命名空间Pipe)中查找所有方法。 这是它给我们的结果:
在类型I中,我们找到了Void SomeMethod()
空! 在IExtensions类型中找不到方法SomeMethod
在类型A中,我们发现了Void SomeMethod()
空! 在类型A中找不到方法OtherMethod
空! 在类型B中找不到方法SomeMethod
空! 在类型B中找不到方法OtherMethod
我必须马上说,使用MethodInfo类型的对象,可以调用找到的方法。 也就是说,如果反射发现了某些东西,那么纯理论上就可以违反封装。 而且我们发现了一些东西。 首先,来自类A的同一个公共void SomeMethod()。期望如此,还有什么要说的。 这种放纵可能仍然会带来后果。 其次,从接口I撤消SomeMethod()。这已经更加有趣了。 无论我们如何锁定自己,放置在接口中的抽象方法(或CLR实际放置在其中的内容)实际上都是打开的。 因此,在单独的段落中得出的结论是:
仔细查看向谁以及您提供什么类型的System.Type类型。
但是找到这两种方法还有一点细微差别,我想考虑一下。 内部类的内部接口方法和公共方法可以使用反射找到。 作为一个合理的人,我将得出结论,它们属于元数据。 作为一个有经验的人,我将验证这个结论。 在此ILDasm将为我们提供帮助。
窥视管道元数据中的兔子洞组装在发布中组装
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
快速浏览显示,无论如何标记, 所有内容都进入元数据。 反思仍然仔细地向我们隐瞒了外人不应看到的东西。 因此,很可能内部接口每种方法的额外五行代码并不是那么大的邪恶。 但是,主要结论仍然相同:
仔细查看要赠送给谁和什么类型的System.Type类型。
但是,这当然是在所有不需要公开访问的地方都使用关键字internal之后的下一个级别。
聚苯乙烯
您知道在程序集中的任何地方都可以使用internal关键字吗? 当它增长时,您必须将其分为两个或多个。 在此过程中,您必须休息一下以打开某些类型。 您必须仔细考虑哪些类型值得公开。 至少简短。
这意味着以下内容: 这种编写代码的实践将使您重新考虑新生程序集之间的体系结构边界将采用何种形状。 还有什么能更美丽?
PPS
从C#7.2版本开始,出现了一个新的访问修饰符,受私有保护。 而且我仍然不知道它是什么以及与它一起吃什么。 既然在实践中没有遇到。 但是我很高兴在评论中知道。 但是不是从文档中复制粘贴,而是在实际情况下可能需要此访问修饰符。