📜 ⬆️ ⬇️

Encapsulation for real samurai, or nuances related to the internal keyword in C #

Prologue: internal is new public


Each of us dreamed of a project where everything would be done correctly. It seems quite natural. As soon as you learn about the very possibility of writing good code, as soon as you hear legends about the very code that can be easily read and changed, you immediately catch fire thereby, “Well, now I’ll do everything right, now I’ve been smart and read McConnell.”


image

Such a project happened in my life. Another. And I do it under voluntary supervision, where every line is monitored. Accordingly, not only wanted, but had to do everything right. One of the "correct" was "honor the encapsulation and close to the maximum, because you always have time to open, and then it will be too late to close back." And so I, wherever I could, began to use the access modifier internal for classes instead of public. And, naturally, when you begin to actively use a new language feature for you, some nuances arise. About them in order and I want to tell.


Offensive basic help

Exclusively in order to recall and label.


  • The build is the minimum deployment unit in .NET and one of the basic compilation units. As is correct, it is either .dll or .exe. They say it can be divided into several files, which are called modules.
  • public — an access modifier that indicates that the tagged is accessible to everyone at all.
  • internal is an access modifier that indicates that the tagged is available only inside the assembly.
  • protected - an access modifier that indicates that the tagged is available only to the heirs of the class in which the tagged is located.
  • private — an access modifier that indicates that the tagged one is available only to the class in which it is located. And no one else.


Unit tests and friendly builds


In C ++, there was such a weird feature like friendly classes. Classes could be assigned as friends, and then the encapsulation boundary between them was erased. I suspect that this is not the strangest feature in C ++. Perhaps even the top ten are not included. But shooting oneself in the leg, tying several classes tightly, is somehow too easy, and it would be very hard to come up with a suitable case for this feature.


It was all the more surprising to find out that in .NET there are friendly builds, a sort of rethinking. That is, you can make one assembly see what is hidden behind the internal lock in another assembly. When I found out about this, I was somewhat surprised. Well, why would? What's the point? Who will tightly bind the two assemblies, taking up their separation? Cases when in any incomprehensible situation are molded public, we do not consider in this article.


And then in the same project I began to learn one of the branches of the path of the present samurai: unit testing. And Feng Shui unit tests should be in a separate assembly. For the same feng shui, everything that can be hidden inside the assembly should be hidden inside the assembly. I faced a very, very unpleasant choice. Either the tests will lie side by side and go to the client along with the code that is useful to him, or everything will be covered by the keyword public, how long it was in the damp bread.


And here, somewhere from the bins of my memory, something about friendly assemblies was obtained. It turned out that if you have the “YourAssemblyName” assembly, you can write it like this:


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

And the “YourAssemblyName.Tests” assembly will see what is marked with the internal keyword in “YourAssemblyName”. This line can be entered, just that, in AssemblyInfo.cs, which VS creates specifically for storing such attributes.


Returning an abusive basic reference
In .NET, in addition to already embedded attributes or keywords like abstract, public, internal, static, you can create your own. And hang them on anything: fields, properties, classes, methods, events, and entire assemblies. In C #, you simply write the name of the attribute in square brackets before you hang it. The exception is the assembly itself, since there is no direct indication in the code that “Assembly begins here”. There, before the attribute name, you must add the assembly:

Thus, the wolves remain fed, the sheep are intact, everything that is possible is still hiding inside the assembly, unit tests live in a separate assembly, as expected, and the feature, about which I barely remembered, acquires a reason to use it. Perhaps the only existing reason.


I almost forgot one important point. The actions of the InternalsVisibleTo attribute are one-way.


protected <internal?


So, the situation: A and B were sitting on the pipe.


 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 was destroyed during the code review process, since it is not used outside the assembly, but for some reason it allows itself to have a public access modifier, B caused a compilation error, which in the first few minutes could introduce a stupor.


Basically, the error message is logical. The property accessor cannot disclose more than the property itself. Any will react with understanding if the compiler gives a header for this:


 internal String OtherProperty { get; public set; } 

But claims to this line immediately break the brain:


 internal String OtherProperty { get; protected set; } 

I note that there will be no complaints about this line:


 internal String OtherProperty { get; private set; } 

If you don’t think too much, then the following hierarchy is built in your head:


 public > internal > protected > private 

And this hierarchy seems to even work. Except one place. Where internal> protected. To understand the essence of the claims of the compiler, let's remember what restrictions impose internal and protected. internal - only inside the assembly. protected - only heirs. Notice any heirs. And if class B is marked as public, then in another assembly it is possible to define its heirs. And then the set accessor will really get access to where all the property does not have it. Since the C # compiler is paranoid, it cannot even allow it.


Thank him for that, but we need to give heirs access to the accessor. And especially for such cases there is a protected internal access modifier.


This reference is no longer so offensive.
  • protected internal - an access modifier that indicates that the tagged is available inside the assembly or to the heirs of the class in which the tagged is located.


So if we want the compiler to allow us to use this property and set it in the heirs, we need to do this:


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

And the correct hierarchy of access modifiers looks like this:


 public > protected internal > internal/protected > private 

Interfaces


So, the situation: A, I, B sat on the pipe.


 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. } } } 

Sat exactly and outside the assembly did not fit. But they were rejected by the compiler. Here the essence of the claims is clear from the error message. The interface implementation must be open. Even if the interface itself is closed. It would be logical to tie access of the interface implementation to its availability, but what is not, is not. The interface implementation must be public.


And we have two options here. First: through the creaking and gnashing of teeth, hang the public access modifier on the interface implementation. Second: explicit interface implementation. It looks like this:


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

Please note that in the second case there is no access modifier. To whom is the implementation of the method available? Let's say no one. Simply show an example:


 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(); 

Explicit implementation of interface I means that unless we explicitly cast a variable to type I, there are no methods implementing this interface. Each time writing (b as I) .SomeMethod () can be an overhead. Like ((i) b) .SomeMethod (). And I found two ways to get around this. I thought of one myself, and the second honestly googled.


Method one - factory:


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

Well, or any other pattern that allows you to hide this nuance.


Method two - extension methods:


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

What is surprising, it works. These lines cease to generate an error:


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

After all, the call goes, as IntelliSense in Visual Studio tells us, not to methods of an explicit interface implementation, but to extension methods. And no one forbids contacting them. And interface extension methods can be called on all its implementations.


But one thing remains. Inside the class itself, you need to access this method using the this keyword, otherwise the compiler will not understand that we want to refer to the extension method:


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

And so, and so, in our country or in public, where it should not be, but there it seems to cause no harm, or a little bit of extra code for each internal-interface. Choose the least evil to your liking.


Reflection


I knocked about it painfully when I tried to find a constructor through reflection, which, of course, was labeled as internal by the internal-class. And it turned out that reflection will not give out anything that would not be public. And this, in principle, is logical.


Firstly, reflection, if I remember correctly what smart people wrote in smart books, it’s about finding information in the build metadata. Which, in theory, should not give too much (I thought so, at least). Secondly, the main use of reflection is to make your program extensible. You provide an outsider with some kind of interface (perhaps even in the form of interfaces, fit-ha!). And they implement it and provide plugins, mods, extensions in the form of an assembly loaded on the fly, from which reflection gets them. And of course, your API will be public. That is, it is impossible to look at internal through reflection technically and senselessly from a practical point of view.


Update. Here in the comments it turned out that reflection allows, if it is explicitly asked about it, to reflect on everything. Be it at least internal, at least private. If you are not writing any code analysis tool, try not to do this, please. The text further is still relevant when we are looking for open member types. And in general, do not pass by the comments, there are a lot of interesting things.


This could be the end of reflection, but let's return to the previous example, where A, I, B were sitting on the tube:


 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() { } } } 

The author of class A decided that nothing terrible would happen if the internal-class method was marked as public, so that the compiler didn’t scream, and so that it wouldn’t be necessary to fence more code. The interface is marked as internal, the class that implements it is marked as internal, from the outside it is seemingly impossible to get to the method marked as public.


And then the door opens and the reflection quietly sneaks:


 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}"); } } } 

Study this code, drive it into the studio, if you so wish. Here we are trying using reflection to find all methods from all types of our pipe (namespace Pipe). And what results it gives us:


I found a Void SomeMethod ()
Null! Can't find method SomeMethod in type IExtensions
In Void SomeMethod ()
Null! Can't find method OtherMethod in type A
Null! Can't find method SomeMethod in type B
Null! Can't find method OtherMethod in type B

At once I will say that using an object of type MethodInfo, the found method can be called. That is, if reflection found something, then it is theoretically possible to break the encapsulation. And we have found something. First, that same public void SomeMethod () from class A. It was expected, what else to say. This concession may still have consequences. Secondly, void SomeMethod () from interface I. This is more interesting. No matter how we lock in, the abstract methods placed on the interface (or what the CLR actually places there) are in fact open. Hence the conclusion in a separate paragraph:


Watch carefully to whom and what objects of type System.Type you give.


But here is another nuance with these two found methods that I would like to consider. Methods of internal-interfaces and open methods of internal-classes can be found through reflection. As a reasonable person, I conclude that they fall into the metadata. As an experienced person, I will verify this conclusion. And this will help us ILDasm.


To look with one eye into the rabbit hole of our pipe metadata

The assembly was assembled in the Release mode.


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


A quick inspection reveals that everything goes into the metadata, no matter how it is marked. Reflection is also carefully hiding from us what is not supposed to be seen by strangers. So it may well be that the extra five lines of code for each method of the internal-interface is not such a big evil. However, the main conclusion remains the same:


Watch carefully to whom and what objects of type System.Type you give.


But this is, of course, the next level, after the accession of the keyword internal in all places where there is no need for public.


PS


Do you know what's great about using the keyword internal everywhere inside an assembly? When it grows, you have to divide it into two or more. In the process, you have to take a pause to make some types open. And you have to think about which types deserve to be open. At least briefly.


This means the following: this practice of writing code will make you think once again about what form the architectural boundary between newborn assemblies will take. What can be more beautiful?


Pps


Starting with C # 7.2, a new access modifier private protected has appeared. And I still have no idea what it is, and what it is eaten with. Since it does not come across in practice. But I will be glad to know in the comments. But not copy-paste from the documentation, but real cases when this access modifier may be needed.


')

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


All Articles