Prologue: interne est un nouveau public
Chacun de nous rêvait d'un projet où tout serait bien fait. Cela semble assez naturel. Dès que vous apprenez la possibilité même d'écrire du bon code, dès que vous entendez des légendes sur le même code qui peuvent être facilement lues et modifiées, vous vous allumez immédiatement "bien, maintenant je vais le faire correctement, je suis intelligent et je lis McConnell."

Un tel projet s'est produit dans ma vie. Un autre. Et je le fais sous supervision volontaire, où chaque ligne que je suis. En conséquence, non seulement je le voulais, mais je devais tout faire correctement. L'un des «corrects» était «respectez l'encapsulation et proche du maximum, car vous avez toujours le temps d'ouvrir, puis il sera trop tard pour refermer». Et donc, partout où j'ai pu, j'ai commencé à utiliser le modificateur d'accès interne au lieu de public pour les cours. Et, bien sûr, lorsque vous commencez à utiliser activement une nouvelle fonctionnalité de langue pour vous, certaines nuances apparaissent. Je veux en parler dans l'ordre.
Aide de base offensiveUniquement pour rappeler et étiqueter.
- L'assemblage est la plus petite unité de déploiement dans .NET et l'une des unités de compilation de base. En l'état, il s'agit de .dll ou .exe. Ils disent qu'il peut être divisé en plusieurs fichiers appelés modules.
- public - modificateur d'accès, ce qui signifie qu'il est accessible à tous ceux qui y sont marqués.
- internal - modificateur d'accès, ce qui signifie qu'il est marqué uniquement disponible à l'intérieur de l'assemblage.
- protected - un modificateur d'accès qui indique qu'il est marqué uniquement disponible pour les héritiers de la classe dans laquelle le marqué est situé.
- privé - un modificateur d'accès qui indique qu'il est marqué uniquement disponible pour la classe dans laquelle il se trouve. Et personne d'autre.
Tests unitaires et versions conviviales
En C ++, il y avait une caractéristique aussi étrange que les classes amies. Les cours pouvaient être assignés comme amis, puis la frontière d'encapsulation entre eux était effacée. Je soupçonne que ce n'est pas la fonctionnalité la plus étrange en C ++. Peut-être que même le top dix des plus étranges n'est pas inclus. Mais se tirer une balle dans le pied en reliant plusieurs classes étroitement, est en quelque sorte trop facile, et il est très difficile de trouver un étui approprié pour cette fonctionnalité.
Le plus surprenant a été d'apprendre qu'en .NET il y a des assemblées amicales, une sorte de repenser. Autrement dit, vous pouvez faire voir à un assemblage ce qui est caché derrière le verrou interne dans un autre assemblage. Quand j'ai découvert cela, j'ai été quelque peu surpris. Eh bien, comment le ferait-il, pourquoi? À quoi ça sert? Qui liera étroitement les deux assemblées, engagées dans leur séparation? Les cas où, dans une situation incompréhensible, ils façonnent le public, nous ne les considérons pas dans cet article.
Et puis dans le même projet, j'ai commencé à apprendre l'une des branches du chemin d'un vrai samouraï: le test unitaire. Et dans le Feng Shui, les tests unitaires doivent être dans un assemblage séparé. Pour le même Feng Shui tout ce qui peut être caché à l'intérieur de l'assemblage, vous devez vous cacher à l'intérieur de l'assemblage. J'ai fait face à un choix très, très désagréable. Soit les tests se dérouleront côte à côte et iront au client avec le code qui lui sera utile, soit tout sera couvert par le mot-clé public, depuis combien de temps le pain repose dans l'humidité.
Et ici, quelque part dans les poubelles de ma mémoire, quelque chose a été obtenu sur les assemblées amies. Il s'est avéré que si vous avez l'assembly "YourAssemblyName", alors vous pouvez écrire comme ceci:
[assembly: InternalsVisibleTo("YourAssemblyName.Tests")]
Et l'assembly "YourAssemblyName.Tests" verra ce qui est marqué avec le mot-clé interne dans "YourAssemblyName". Cette ligne peut être entrée, juste un peu, dans AssemblyInfo.cs, que VS crée spécifiquement pour stocker de tels attributs.
Retour abusif à l'aide de baseDans .NET, en plus d'attributs ou de mots clés déjà intégrés comme abstrait, public, interne, statique, vous pouvez créer le vôtre. Et accrochez-les sur tout ce que vous voulez: champs, propriétés, classes, méthodes, événements et assemblages entiers. En C #, pour cela, vous écrivez simplement le nom de l'attribut entre crochets avant ce à quoi vous vous accrochez. L'exception est l'assemblage lui-même, car il n'y a aucune indication directe nulle part dans le code que "l'assemblage commence ici". Là, avant le nom de l'attribut, vous devez ajouter l'assembly:
Ainsi, les loups restent pleins, les moutons sont en sécurité, tout ce qui est possible se cache toujours à l'intérieur de l'assemblage, les tests unitaires vivent dans un assemblage séparé, comme il se doit, et une fonctionnalité dont je me souviens à peine obtient une raison de l'utiliser. Peut-être la seule raison existante.
J'ai presque oublié un point important. L'action d'attribut InternalsVisibleTo est unidirectionnelle.
protégé <interne?
Donc la situation: A et B étaient assis sur une pipe.
using System; namespace Pipe { public class A { public String SomeProperty { get; protected set; } } internal class B {
A a été détruit dans le processus de révision du code, car il n'est pas utilisé en dehors de l'assembly, mais pour une raison quelconque, se permet d'avoir un modificateur d'accès public, B a provoqué une erreur de compilation, ce qui pourrait entraîner une stupeur dans les premières minutes.
Fondamentalement, le message d'erreur est logique. L'accesseur de propriété ne peut pas révéler plus que la propriété elle-même. N'importe qui réagira avec compréhension si le compilateur donne un en-tête pour cela:
internal String OtherProperty { get; public set; }
Mais les revendications sur cette ligne brisent immédiatement le cerveau:
internal String OtherProperty { get; protected set; }
Je note qu'il n'y aura aucune réclamation concernant cette ligne:
internal String OtherProperty { get; private set; }
Si vous ne pensez pas beaucoup, la hiérarchie suivante est construite dans votre tête:
public > internal > protected > private
Et cette hiérarchie semble même fonctionner. Sauf pour un endroit. Où interne> protégé. Pour comprendre l'essence des revendications du compilateur, rappelons quelles restrictions sont imposées par internes et protégées. interne - uniquement à l'intérieur de l'assemblage. protégés - seuls héritiers. Remarquez les héritiers. Et si la classe B est marquée comme publique, alors dans un autre assembly, vous pouvez définir ses descendants. Et puis l'accesseur set obtient vraiment accès là où la propriété entière ne l'a pas. Puisque le compilateur C # est paranoïaque, il ne peut même pas permettre une telle possibilité.
Merci à lui pour cela, mais nous devons donner aux héritiers l'accès à l'accesseur. Et spécifiquement pour de tels cas, il existe un modificateur d'accès interne protégé.
Cette aide n'est pas si offensante- protégé interne - un modificateur d'accès qui indique que celui marqué est disponible à l'intérieur de l'assemblage ou aux héritiers de la classe dans laquelle se trouve celui marqué.
Donc, si nous voulons que le compilateur nous permette d'utiliser cette propriété et de la définir dans les héritiers, nous devons le faire:
using System; namespace Pipe { internal class B { protected internal String OtherProperty { get; protected set; } } }
Et la hiérarchie correcte des modificateurs d'accès ressemble à ceci:
public > protected internal > internal/protected > private
Interfaces
Donc, la situation: A, I, B étaient assis sur le tuyau.
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { internal void SomeMethod() {
Nous nous sommes assis exactement et ne nous sommes pas mêlés de l'extérieur de l'assemblée. Mais ils ont été rejetés par le compilateur. Ici, l'essence des revendications ressort clairement du message d'erreur. L'implémentation de l'interface doit être ouverte. Même si l'interface elle-même est fermée. Il serait logique de lier l'accès à la mise en œuvre de l'interface à sa disponibilité, mais ce qui ne l'est pas, ne l'est pas. L'implémentation de l'interface doit être publique.
Et nous avons deux issues. Premièrement: à travers le grincement et les grincements de dents, accrochez un modificateur d'accès public à la mise en œuvre de l'interface. Deuxièmement: implémentation explicite de l'interface. Cela ressemble à ceci:
namespace Pipe { internal interface I { void SomeMethod(); } internal class A : I { public void SomeMethod() { } } internal class B : I { void I.SomeMethod() { } } }
Veuillez noter que dans le deuxième cas, il n'y a pas de modificateur d'accès. À qui dans ce cas la mise en œuvre de la méthode est-elle disponible? Disons simplement que personne. Il est plus facile de montrer avec un exemple:
B b = new B();
L'implémentation explicite de l'interface I signifie que tant que nous ne convertissons pas explicitement la variable en type I, aucune méthode n'implémente cette interface. L'écriture (b comme I) .SomeMethod () à chaque fois peut être une surcharge. Comme ((I) b) .Quelque méthode (). Et j'ai trouvé deux façons de contourner cela. J'ai pensé à un moi-même et honnêtement googlé le second.
La première façon est l'usine:
internal class Factory { internal I Create() { return new B(); } }
Eh bien, ou tout autre motif qui vous permet de masquer cette nuance.
Deuxième méthode - méthodes d'extension:
internal static class IExtensions { internal static void SomeMethod(this I i) { i.SomeMethod(); } }
Étonnamment, cela fonctionne. Ces lignes cessent de générer une erreur:
B b = new B(); b.SomeMethod();
Après tout, l'appel vient, comme nous le dit IntelliSense dans Visual Studio, non pas aux méthodes pour implémenter explicitement l'interface, mais aux méthodes d'extension. Et personne n'interdit de se tourner vers eux. Et les méthodes d'extension d'interface peuvent être appelées sur toutes ses implémentations.
Mais il reste une mise en garde. À l'intérieur de la classe elle-même, vous devez accéder à cette méthode via le mot-clé this, sinon le compilateur ne comprendra pas que nous voulons faire référence à la méthode d'extension:
internal class B : I { internal void OtherMethod() {
Et ainsi, et ainsi, nous avons ou public, où il ne devrait pas être, mais là, il ne semble pas faire de mal, ou un peu de code supplémentaire pour chaque interface interne. Choisissez le moindre mal à votre goût.
La réflexion
Je l'ai frappé douloureusement lorsque j'ai essayé de trouver un constructeur par la réflexion, qui, bien sûr, était marquée comme interne dans la classe interne. Et il s'est avéré que la réflexion ne donnera rien qui ne soit pas public. Et cela, en principe, est logique.
Tout d'abord, réflexion, si je me souviens bien de ce que les gens intelligents ont écrit dans les livres intelligents, il s'agit de trouver des informations dans les métadonnées d'assemblage. Ce qui, en théorie, ne devrait pas trop en révéler (je le pensais du moins). Deuxièmement, l'utilisation principale de la réflexion est de rendre votre programme extensible. Vous fournissez une sorte d'interface aux étrangers (peut-être même sous forme d'interfaces, fiy-ha!). Et ils l'implémentent et fournissent des plugins, des mods, des extensions sous la forme d'un assemblage chargé en déplacement, d'où la réflexion les obtient. Et en soi, votre API sera publique. C'est-à-dire que regarder l'intérieur par la réflexion n'est pas techniquement et inutile d'un point de vue pratique.
Mettre à jour Ici, dans les commentaires, il s'est avéré que la réflexion permet, si vous le demandez explicitement, de tout refléter. Que ce soit même interne, même privé. Si vous n'écrivez pas une sorte d'outil d'analyse de code, essayez de ne pas le faire, s'il vous plaît. Le texte ci-dessous est toujours pertinent pour les cas où nous recherchons des types de membres ouverts. Et en général, ne passez pas de commentaires, il y a beaucoup de choses intéressantes.
Cela pourrait être terminé par la réflexion, mais revenons à l'exemple précédent, où A, I, B étaient assis sur un tuyau:
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() { } } }
L'auteur de la classe A a décidé qu'il ne se passerait rien de mal si la méthode de la classe interne était marquée comme publique, de sorte que le compilateur ne souffrait pas et qu'il n'était pas nécessaire de mettre plus de code dedans. L'interface est marquée comme interne, la classe qui l'implémente est marquée comme interne, de l'extérieur il semble qu'il n'y ait aucun moyen d'accéder à la méthode marquée comme publique.
Et puis la porte s'ouvre et la réflexion se glisse doucement:
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}"); } } }
Étudiez ce code, introduisez-le dans le studio, si vous le souhaitez. Ici, nous essayons d'utiliser la réflexion pour trouver toutes les méthodes de tous les types de notre pipe (pipe namespace). Et voici les résultats qu'il nous donne:
Dans le type I, nous avons trouvé Void SomeMethod ()
NULL! Impossible de trouver la méthode SomeMethod dans le type IExtensions
Dans le type A, nous avons trouvé Void SomeMethod ()
NULL! Impossible de trouver la méthode OtherMethod dans le type A
NULL! Impossible de trouver la méthode SomeMethod dans le type B
NULL! Impossible de trouver la méthode OtherMethod dans le type B
Je dois dire tout de suite qu'en utilisant un objet de type MethodInfo, la méthode trouvée peut être appelée. Autrement dit, si la réflexion a trouvé quelque chose, alors l'encapsulation peut être violée purement théoriquement. Et nous avons trouvé quelque chose. Tout d'abord, le même public annule SomeMethod () de la classe A. On s'attendait à quoi d'autre à dire. Cette indulgence peut encore avoir des conséquences. Deuxièmement, annulez SomeMethod () de l'interface I. C'est déjà plus intéressant. Peu importe comment nous nous enfermons, les méthodes abstraites placées dans l'interface (ou ce que le CLR y place réellement) sont réellement ouvertes. D'où la conclusion d'un paragraphe distinct:
Examinez attentivement qui et quel type de type de système que vous donnez.
Mais il y a une nuance de plus avec ces deux méthodes trouvées, que je voudrais considérer. Les méthodes d'interface interne et les méthodes publiques des classes internes peuvent être trouvées en utilisant la réflexion. En tant que personne raisonnable, je conclurai qu'ils font partie des métadonnées. En tant que personne expérimentée, je vérifierai cette conclusion. Et dans cet ILDasm nous aidera.
Jetez un œil au trou du lapin dans les métadonnées de notre pipeL'assemblage a été assemblé dans la version 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
Un coup d'œil rapide montre que tout entre dans les métadonnées, peu importe comment elles sont marquées. La réflexion nous cache toujours soigneusement que les étrangers ne sont pas censés voir. Il se peut donc bien que les cinq lignes de code supplémentaires pour chaque méthode de l'interface interne ne soient pas un si grand mal. Cependant, la principale conclusion reste la même:
Examinez attentivement qui et quel type de type de système que vous donnez.
Mais c'est, bien sûr, le niveau suivant, après l'adhésion du mot-clé interne à tous les endroits où il n'y a pas besoin de public.
PS
Vous savez que la chose la plus cool à propos de l'utilisation du mot-clé interne est partout dans l'assemblage? Quand il grandit, vous devez le diviser en deux ou plus. Et dans le processus, vous devez faire une pause pour ouvrir certains types. Et vous devez penser exactement aux types qui méritent d'être ouverts. Au moins brièvement.
Cela signifie ce qui suit: cette pratique d'écriture de code vous fera réfléchir à nouveau sur la forme que prendra la frontière architecturale entre les assemblages de nouveau-nés. Quoi de plus beau?
PPS
À partir de la version C # 7.2, un nouveau modificateur d'accès, privé protégé, est apparu. Et je n'ai toujours aucune idée de ce que c'est et avec quoi on le mange. Depuis pas rencontré dans la pratique. Mais je serai heureux de savoir dans les commentaires. Mais pas le copier-coller de la documentation, mais des cas réels où ce modificateur d'accès peut être nécessaire.