Prolog: intern ist neue Öffentlichkeit
Jeder von uns träumte von einem Projekt, bei dem alles richtig gemacht würde. Es scheint ganz natürlich. Sobald Sie von der Möglichkeit erfahren, guten Code zu schreiben, und sobald Sie Legenden über den Code hören, der leicht gelesen und geändert werden kann, leuchten Sie sofort auf: "Nun, jetzt mache ich es richtig, ich bin schlau und lese McConnell."

Ein solches Projekt ist in meinem Leben passiert. Noch einer. Und ich mache es unter freiwilliger Aufsicht, wo jede Linie, der ich folge. Dementsprechend wollte ich nicht nur, sondern ich musste alles richtig machen. Eines der „richtigen“ war „Respektieren Sie die Kapselung und nähern Sie sich dem Maximum, weil Sie immer Zeit zum Öffnen haben und es dann zu spät ist, um es wieder zu schließen“. Und deshalb begann ich, wo immer ich konnte, den internen Zugriffsmodifikator anstelle des öffentlichen für Klassen zu verwenden. Und natürlich treten einige Nuancen auf, wenn Sie beginnen, eine neue Sprachfunktion aktiv für Sie zu verwenden. Ich möchte der Reihe nach darüber sprechen.
Offensive GrundhilfeNur zum Erinnern und Beschriften.
- Assembly ist die kleinste Bereitstellungseinheit in .NET und eine der grundlegenden Kompilierungseinheiten. Dies ist entweder .dll oder .exe. Sie sagen, dass es in mehrere Dateien unterteilt werden kann, die Module genannt werden.
- public - access modifier, was bedeutet, dass er für alle mit ihm gekennzeichneten zugänglich ist.
- Modifikator für internen Zugriff, dh, er ist nur innerhalb der Baugruppe verfügbar.
- protected - Ein Zugriffsmodifikator, der angibt, dass die Markierung nur den Erben der Klasse zur Verfügung steht, in der sich die Markierung befindet.
- privat - Ein Zugriffsmodifikator, der angibt, dass er nur für die Klasse verfügbar ist, in der er sich befindet. Und sonst niemand.
Unit Tests und Friendly Builds
In C ++ gab es eine so seltsame Funktion wie freundliche Klassen. Klassen konnten als Freunde zugewiesen werden, und dann wurde die Grenze der Kapselung zwischen ihnen gelöscht. Ich vermute, dass dies nicht die seltsamste Funktion in C ++ ist. Vielleicht sind sogar die zehn seltsamsten nicht enthalten. Es ist jedoch zu einfach, sich durch die enge Verknüpfung mehrerer Klassen in den Fuß zu schießen, und es ist sehr schwierig, einen geeigneten Fall für diese Funktion zu finden.
Umso überraschender war es zu erfahren, dass es in .NET freundliche Assemblys gibt, eine Art Umdenken. Das heißt, Sie können einer Baugruppe zeigen lassen, was in einer anderen Baugruppe hinter dem internen Schloss verborgen ist. Als ich davon erfuhr, war ich etwas überrascht. Nun, wie würde, warum? Was ist der Punkt? Wer wird die beiden Versammlungen, die an ihrer Trennung beteiligt sind, fest verbinden? Fälle, in denen sie in einer unverständlichen Situation öffentlich werden, werden in diesem Artikel nicht berücksichtigt.
Und dann begann ich im selben Projekt, einen der Zweige des Weges eines echten Samurai zu lernen: Unit-Tests. Und im Feng Shui sollten Unit-Tests in einer separaten Baugruppe durchgeführt werden. Für dasselbe Feng Shui müssen Sie alles, was in der Baugruppe verborgen werden kann, in der Baugruppe verstecken. Ich stand vor einer sehr, sehr unangenehmen Wahl. Entweder liegen die Tests nebeneinander und gehen zusammen mit dem für ihn nützlichen Code an den Kunden, oder alles wird durch das Schlüsselwort public abgedeckt, wie lange das Brot in der Feuchtigkeit gelegen hat.
Und hier, irgendwo in den Behältern meiner Erinnerung, wurde etwas über freundliche Versammlungen erhalten. Es stellte sich heraus, dass Sie mit der Assembly "YourAssemblyName" folgendermaßen schreiben können:
[assembly: InternalsVisibleTo("YourAssemblyName.Tests")]
In der Assembly "YourAssemblyName.Tests" wird angezeigt, was in "YourAssemblyName" mit dem internen Schlüsselwort gekennzeichnet ist. Diese Zeile kann nur ein wenig in AssemblyInfo.cs eingegeben werden, das VS speziell zum Speichern solcher Attribute erstellt.
Kehren Sie missbräuchlich zur grundlegenden Hilfe zurückIn .NET können Sie zusätzlich zu bereits integrierten Attributen oder Schlüsselwörtern wie abstrakt, öffentlich, intern und statisch eigene Attribute erstellen. Und hängen Sie sie an alles, was Sie wollen: Felder, Eigenschaften, Klassen, Methoden, Ereignisse und ganze Assemblys. In C # schreiben Sie dazu einfach den Attributnamen in eckige Klammern, bevor Sie daran festhalten. Die Ausnahme ist die Assembly selbst, da es im Code keinen direkten Hinweis darauf gibt, dass "Assembly beginnt hier". Dort müssen Sie vor dem Attributnamen eine Assembly hinzufügen:
So bleiben die Wölfe voll, die Schafe sind in Sicherheit, alles, was möglich ist, versteckt sich immer noch in der Baugruppe, Unit-Tests werden in einer separaten Baugruppe durchgeführt, wie es sein sollte, und eine Funktion, an die ich mich kaum erinnerte, bekommt einen Grund, sie zu verwenden. Vielleicht der einzige existierende Grund.
Ich habe fast einen wichtigen Punkt vergessen. Die Attributaktion InternalsVisibleTo ist einseitig.
geschützt <intern?
Also die Situation: A und B saßen auf einer Pfeife.
using System; namespace Pipe { public class A { public String SomeProperty { get; protected set; } } internal class B {
A wurde bei der Codeüberprüfung zerstört, da es nicht außerhalb der Assembly verwendet wird, sondern sich aus irgendeinem Grund einen Modifikator für den öffentlichen Zugriff zulässt. B verursachte einen Kompilierungsfehler, der in den ersten Minuten zu einer Betäubung führen kann.
Grundsätzlich ist die Fehlermeldung logisch. Der Immobilien-Accessor kann nicht mehr als die Immobilie selbst preisgeben. Jeder wird mit Verständnis reagieren, wenn der Compiler einen Header dafür angibt:
internal String OtherProperty { get; public set; }
Aber Behauptungen zu dieser Linie brechen sofort das Gehirn:
internal String OtherProperty { get; protected set; }
Ich stelle fest, dass es zu dieser Zeile keine Beschwerden geben wird:
internal String OtherProperty { get; private set; }
Wenn Sie nicht viel nachdenken, wird die folgende Hierarchie in Ihrem Kopf aufgebaut:
public > internal > protected > private
Und diese Hierarchie scheint sogar zu funktionieren. Bis auf einen Ort. Wo intern> geschützt. Um das Wesentliche der Ansprüche des Compilers zu verstehen, erinnern wir uns daran, welche Einschränkungen durch interne und geschützte auferlegt werden. intern - nur innerhalb der Baugruppe. geschützt - nur Erben. Beachten Sie alle Erben. Und wenn Klasse B als öffentlich markiert ist, können Sie in einer anderen Assembly ihre Nachkommen definieren. Und dann bekommt der Set-Accessor wirklich Zugriff darauf, wo die gesamte Eigenschaft es nicht hat. Da der C # -Compiler paranoid ist, kann er eine solche Möglichkeit nicht einmal zulassen.
Vielen Dank an ihn dafür, aber wir müssen den Erben Zugang zum Accessor gewähren. Und speziell für solche Fälle gibt es einen geschützten internen Zugriffsmodifikator.
Diese Hilfe ist nicht so anstößig- protected internal - Ein Zugriffsmodifikator, der angibt, dass der markierte innerhalb der Assembly oder für die Erben der Klasse verfügbar ist, in der sich der markierte befindet.
Wenn wir also möchten, dass der Compiler uns erlaubt, diese Eigenschaft zu verwenden und sie in den Erben festzulegen, müssen wir Folgendes tun:
using System; namespace Pipe { internal class B { protected internal String OtherProperty { get; protected set; } } }
Und die richtige Hierarchie der Zugriffsmodifikatoren sieht ungefähr so aus:
public > protected internal > internal/protected > private
Schnittstellen
Also die Situation: A, ich, B saßen auf der Pfeife.
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { internal void SomeMethod() {
Wir saßen genau und mischten uns nicht außerhalb der Versammlung ein. Sie wurden jedoch vom Compiler abgelehnt. Hier wird das Wesentliche der Ansprüche aus der Fehlermeldung deutlich. Die Implementierung der Schnittstelle muss offen sein. Auch wenn die Schnittstelle selbst geschlossen ist. Es wäre logisch, den Zugriff auf die Implementierung der Schnittstelle an ihre Verfügbarkeit zu binden, aber was nicht ist, ist nicht. Die Implementierung der Schnittstelle muss öffentlich sein.
Und wir haben zwei Auswege. Erstens: Hängen Sie durch Knarren und Zähneknirschen einen Modifikator für den öffentlichen Zugriff an die Implementierung der Schnittstelle. Zweitens: explizite Implementierung der Schnittstelle. Es sieht so aus:
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { public void SomeMethod() { } } internal class B : I { void I.SomeMethod() { } } }
Bitte beachten Sie, dass im zweiten Fall kein Zugriffsmodifikator vorhanden ist. Wem steht in diesem Fall die Implementierung der Methode zur Verfügung? Sagen wir einfach niemand. Es ist einfacher, mit einem Beispiel zu zeigen:
B b = new B();
Die explizite Implementierung von Schnittstelle I bedeutet, dass es keine Methoden gibt, die diese Schnittstelle implementieren, bis wir die Variable explizit in Typ I umwandeln. Das Schreiben (b als I) .SomeMethod () kann jedes Mal eine Überlastung sein. Wie ((I) b) .SomeMethod (). Und ich habe zwei Wege gefunden, um das zu umgehen. Ich dachte selbst an einen und googelte ehrlich den zweiten.
Der erste Weg ist die Fabrik:
internal class Factory { internal I Create() { return new B(); } }
Nun, oder irgendein anderes Muster, mit dem Sie diese Nuance verbergen können.
Methode zwei - Erweiterungsmethoden:
internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } }
Überraschenderweise funktioniert es. Diese Zeilen geben keinen Fehler mehr aus:
B b = new B(); b.SomeMethod();
Schließlich kommt der Aufruf, wie IntelliSense in Visual Studio mitteilt, nicht zu Methoden zur expliziten Implementierung der Schnittstelle, sondern zu Erweiterungsmethoden. Und niemand verbietet es, sich an sie zu wenden. Und Schnittstellenerweiterungsmethoden können für alle Implementierungen aufgerufen werden.
Es bleibt jedoch eine Einschränkung. Innerhalb der Klasse selbst müssen Sie über das Schlüsselwort this auf diese Methode zugreifen, da der Compiler sonst nicht versteht, dass wir auf die Erweiterungsmethode verweisen möchten:
internal class B : I { internal void OtherMethod() {
Und so und so haben wir oder öffentlich, wo es nicht sein sollte, aber dort scheint es keinen Schaden zuzufügen, oder ein wenig zusätzlichen Code für jede interne Schnittstelle. Wählen Sie das kleinere Übel nach Ihren Wünschen.
Reflexion
Ich traf dies schmerzhaft, als ich versuchte, durch Reflexion einen Konstruktor zu finden, der natürlich in der internen Klasse als intern markiert war. Und es stellte sich heraus, dass Reflexion nichts preisgibt, was nicht öffentlich wäre. Und das ist im Prinzip logisch.
Erstens, wenn ich mich richtig erinnere, was intelligente Leute in intelligenten Büchern geschrieben haben, geht es darum, Informationen in den Assembly-Metadaten zu finden. Was theoretisch nicht zu viel geben sollte (zumindest dachte ich das). Zweitens besteht die Hauptanwendung von Reflection darin, Ihr Programm erweiterbar zu machen. Sie bieten Außenstehenden eine Art Schnittstelle (vielleicht sogar in Form von Schnittstellen, fiy-ha!). Und sie implementieren es und stellen Plugins, Mods und Erweiterungen in Form einer Assembly bereit, die unterwegs geladen wird und von der Reflexion sie erhält. Und an sich wird Ihre API öffentlich sein. Das heißt, das Betrachten von Innen durch Reflexion ist aus praktischer Sicht technisch und sinnlos nicht.
Update Hier in den Kommentaren stellte sich heraus, dass Reflexion es erlaubt, wenn Sie explizit danach fragen, alles zu reflektieren. Sei es sogar intern, sogar privat. Wenn Sie kein Code-Analyse-Tool schreiben, versuchen Sie dies bitte nicht. Der folgende Text ist weiterhin relevant für Fälle, in denen wir nach offenen Mitgliedertypen suchen. Und im Allgemeinen keine Kommentare weitergeben, es gibt viele interessante Dinge.
Dies könnte mit Reflexion beendet werden, aber kehren wir zum vorherigen Beispiel zurück, in dem A, I, B auf einem Rohr saßen:
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() { } } }
Der Autor der Klasse A entschied, dass nichts Schlimmes passieren würde, wenn die Methode der internen Klasse als öffentlich markiert würde, so dass der Compiler keine Schmerzen hatte und kein weiterer Code eingefügt werden musste. Die Schnittstelle ist als intern markiert, die Klasse, die sie implementiert, ist als intern markiert. Von außen scheint es keine Möglichkeit zu geben, zu der als öffentlich gekennzeichneten Methode zu gelangen.
Und dann öffnet sich die Tür und das Spiegelbild schleicht sich leise ein:
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}"); } } }
Studieren Sie diesen Code und fahren Sie ihn ins Studio, wenn Sie dies wünschen. Hier versuchen wir, mithilfe der Reflexion alle Methoden aus allen Arten unserer Pipe (Namespace Pipe) zu finden. Und hier sind die Ergebnisse, die es uns gibt:
In Typ I fanden wir Void SomeMethod ()
NULL! Die Methode SomeMethod kann vom Typ IExtensions nicht gefunden werden
In Typ A fanden wir Void SomeMethod ()
NULL! Die Methode OtherMethod in Typ A kann nicht gefunden werden
NULL! Die Methode SomeMethod in Typ B kann nicht gefunden werden
NULL! Die Methode OtherMethod in Typ B kann nicht gefunden werden
Ich muss sofort sagen, dass mit einem Objekt vom Typ MethodInfo die gefundene Methode aufgerufen werden kann. Das heißt, wenn die Reflexion etwas gefunden hat, kann die Einkapselung rein theoretisch verletzt werden. Und wir haben etwas gefunden. Erstens, die gleiche öffentliche Leere SomeMethod () aus Klasse A. Es wurde erwartet, was noch zu sagen ist. Dieser Genuss kann immer noch Konsequenzen haben. Zweitens void SomeMethod () von Schnittstelle I. Dies ist bereits interessanter. Unabhängig davon, wie wir uns einsperren, sind die abstrakten Methoden in der Schnittstelle (oder was die CLR tatsächlich dort platziert) tatsächlich offen. Daher die Schlussfolgerung in einem separaten Absatz:
Schauen Sie sich genau an, wen und welche Art von System.Type-Typ Sie verschenken.
Bei diesen beiden Methoden gibt es jedoch noch eine weitere Nuance, die ich berücksichtigen möchte. Interne Schnittstellenmethoden und öffentliche Methoden interner Klassen können mithilfe von Reflection gefunden werden. Als vernünftige Person werde ich zu dem Schluss kommen, dass sie in die Metadaten fallen. Als erfahrene Person werde ich diese Schlussfolgerung überprüfen. Und in diesem ILDasm wird uns helfen.
Werfen Sie einen Blick auf das Kaninchenloch in den Metadaten unserer PipeMontage wurde in Release montiert
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
Ein kurzer Blick zeigt, dass alles in die Metadaten gelangt, egal wie sie markiert sind. Reflexion verbirgt uns immer noch sorgfältig, dass Außenstehende nicht sehen sollen. Es kann also durchaus sein, dass die zusätzlichen fünf Codezeilen für jede Methode der internen Schnittstelle kein so großes Übel sind. Die Hauptschlussfolgerung bleibt jedoch dieselbe:
Schauen Sie sich genau an, wen und welche Art von System.Type-Typ Sie verschenken.
Dies ist natürlich die nächste Stufe nach dem Zugriff auf das interne Schlüsselwort an allen Stellen, an denen keine öffentliche Notwendigkeit besteht.
PS
Sie wissen, dass das Coolste an der Verwendung des internen Schlüsselworts überall in der Assembly ist? Wenn es wächst, müssen Sie es in zwei oder mehr teilen. Dabei müssen Sie eine Pause einlegen, um einige Typen zu öffnen. Und Sie müssen genau darüber nachdenken, welche Typen es wert sind, offen zu werden. Zumindest kurz.
Dies bedeutet Folgendes: Diese Praxis des Schreibens von Code lässt Sie erneut darüber nachdenken, wie die architektonische Grenze zwischen neugeborenen Baugruppen aussehen wird. Was könnte schöner sein?
PPS
Ab Version C # 7.2 wurde ein neuer, privat geschützter Zugriffsmodifikator angezeigt. Und ich habe immer noch keine Ahnung, was es ist und womit es gegessen wird. Da in der Praxis nicht angetroffen. Aber ich werde es gerne in den Kommentaren wissen. Aber nicht Kopieren und Einfügen aus der Dokumentation, sondern echte Fälle, in denen dieser Zugriffsmodifikator möglicherweise benötigt wird.