実際のサムライのカプセル化、またはC#の内部キーワードに関連付けられたニュアンス

プロローグ:内部は新しいパブリックです



私たちは皆、すべてが正しく行われるプロジェクトを夢見ていました。 とても自然に思えます。 良いコードを書くことの可能性について知るとすぐに、同じコードについて簡単に読んだり修正したりできる伝説が聞こえるとすぐに、それですぐに点灯します。







画像



このようなプロジェクトは私の人生で起こりました。 もう一つ。 そして、私は任意の監督の下でそれを行います。 したがって、私がしたかっただけでなく、すべてを正しくしなければなりませんでした。 「正しい」ことの1つは、「カプセル化を尊重し、最大限に近いことです。常に開く時間があるため、閉じるには遅すぎます」。 したがって、できる限り、パブリックではなくクラスに内部アクセス修飾子を使用し始めました。 そしてもちろん、あなたが新しい言語機能を積極的に使用し始めると、いくつかのニュアンスが生じます。 それらについて順番に、私は伝えたいです。







攻撃的な基本ヘルプ

思い出させてラベルを付けるだけです。







  • アセンブリは、.NETでの展開の最小単位であり、基本的なコンパイル単位の1つです。 現状では、.dllまたは.exeです。 彼らは、モジュールと呼ばれるいくつかのファイルに分割できると言っています。
  • public-アクセス修飾子。これは、マークされたすべてのユーザーがアクセスできることを意味します。
  • internal-アクセス修飾子。つまり、アセンブリ内でのみ使用可能とマークされます。
  • protected-マークされているクラスの相続人のみがアクセスできるようにマークされていることを示すアクセス修飾子。
  • private-配置されているクラスでのみ使用可能とマークされていることを示すアクセス修飾子。 そして誰もいない。




単体テストとフレンドリービルド



C ++では、フレンドリークラスのような奇妙な機能がありました。 クラスをフレンドとして割り当てると、クラス間のカプセル化の境界が消去されました。 これは、C ++で最も奇妙な機能ではないと思われます。 おそらく、最も奇妙なトップ10も含まれていません。 しかし、いくつかのクラスを緊密にリンクして足元を撃つことは、どういうわけか簡単すぎて、この機能に適したケースを見つけることは非常に困難です。







.NETには、フレンドリーなアセンブリがあり、一種の再考があることを知るのは、さらに驚くべきことでした。 つまり、あるアセンブリに、別のアセンブリの内部ロックの後ろに隠れているものを表示させることができます。 これを知ったとき、私はいくらか驚きました。 さて、どのように、なぜですか? ポイントは何ですか? 分離に関係する2つのアセンブリを誰がしっかりとバインドしますか? 不可解な状況で公開された場合、この記事では考慮しません。







そして、同じプロジェクトで、私は本物のsaの道の分岐の1つであるユニットテストを学び始めました。 また、風水では単体テストを別のアセンブリで行う必要があります。 同じ風水でアセンブリ内に隠すことができるすべてのものについては、アセンブリ内に隠す必要があります。 私は非常に不快な選択に直面しました。 テストは横に並んで、有用なコードと一緒にクライアントに送られるか、すべてがキーワードpublicでカバーされます。これは、パンが湿っている時間です。







そして、ここで、私の記憶のどこかから、友好的な集会について何かが得られました。 アセンブリ "YourAssemblyName"がある場合、次のように記述できることがわかりました。







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





また、アセンブリ「YourAssemblyName.Tests」では、「YourAssemblyName」の内部キーワードでマークされたものが表示されます。 この行は、AssemblyInfo.csで入力できます。AssemblyInfo.csは、VSがそのような属性を保存するために特別に作成します。







虐待を基本的なヘルプに戻す
.NETでは、抽象、パブリック、内部、静的などの既に組み込まれている属性やキーワードに加えて、独自の属性を作成できます。 そして、フィールド、プロパティ、クラス、メソッド、イベント、アセンブリ全体など、必要なものにそれらを掛けます。 C#では、これを行うには、単純に属性名を角かっこで囲んで記述します。 「アセンブリはここから始まる」というコードのどこにも直接示されていないため、例外はアセンブリ自体です。 そこで、属性名の前にアセンブリを追加します。



したがって、オオカミはうんざりしたままで、羊は安全であり、可能なものはすべてアセンブリ内にまだ隠れています。ユニットテストは期待どおり別のアセンブリに住んでおり、私がかろうじて思い出した機能にはそれを使用する理由があります。 おそらく唯一の既存の理由。







私は1つの重要な点をほとんど忘れていました。 属性アクションInternalsVisibleToは一方向です。







保護されている<内部?



状況:AとBはパイプの上に座っていました。







 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はアセンブリの外部で使用されないため、コードレビュープロセスで破壊されましたが、何らかの理由でパブリックアクセス修飾子を持つことができるため、Bはコンパイルエラーを引き起こし、最初の数分間で混乱に陥ることがありました。







基本的に、エラーメッセージは論理的です。 プロパティアクセサーは、プロパティ自体を公開することはできません。 コンパイラがこれにヘッダーを提供すると、誰でも理解して反応します。







 internal String OtherProperty { get; public set; }
      
      





しかし、この行に対する主張はすぐに脳を破壊します:







 internal String OtherProperty { get; protected set; }
      
      





この行について不満はないことに注意してください。







 internal String OtherProperty { get; private set; }
      
      





よく考えなければ、次の階層が頭の中に構築されます。







 public > internal > protected > private
      
      





そして、この階層はうまく機能しているようです。 一箇所を除きます。 where internal> protected。 コンパイラーの主張の本質を理解するために、内部および保護によって課せられる制限を思い出しましょう。 内部-アセンブリ内部のみ。 保護-相続人のみ。 相続人に注意してください。 クラスBがパブリックとしてマークされている場合、別のアセンブリでその子孫を定義できます。 そして、設定されたアクセサは、プロパティ全体に存在しない場所に実際にアクセスします。 C#コンパイラは妄想的であるため、そのような可能性さえ許すことはできません。







彼に感謝しますが、継承者にアクセサへのアクセスを許可する必要があります。 そして、特にそのような場合のために、保護された内部アクセス修飾子があります。







このヘルプはそれほど不快ではありません
  • protected internal-マークされたものがアセンブリ内で、 またはマークされたものが存在するクラス相続人が利用できることを示すアクセス修飾子。




したがって、コンパイラにこのプロパティの使用を許可し、相続人に設定するには、これを行う必要があります。







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





私たちは正確に座って、アセンブリの外側に干渉しませんでした。 しかし、それらはコンパイラによって拒否されました。 ここで、クレームの本質はエラーメッセージから明らかです。 インターフェイスの実装は開いている必要があります。 インターフェイス自体が閉じていても。 インターフェースの実装へのアクセスをその可用性に結び付けることは論理的ですが、そうではないものはそうではありません。 インターフェイスの実装はパブリックでなければなりません。







そして、2つの方法があります。 最初:歯のきしみと歯ぎしりを通して、インターフェイスの実装にパブリックアクセス修飾子を掛けます。 2番目:インターフェイスの明示的な実装。 次のようになります。







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





2番目の場合、アクセス修飾子はありません。 この場合、メソッドの実装は誰に利用可能ですか? 誰も言わないようにしましょう。 例で表示する方が簡単です:







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





インターフェイスIの明示的な実装とは、変数をタイプIに明示的にキャストするまで、このインターフェイスを実装するメソッドがないことを意味します。 (b as I).SomeMethod()を書くたびにオーバーロードになる可能性があります。 Like((I)b).SomeMethod()。 そして、私はこれを回避する2つの方法を見つけました。 私は自分のことを考え、正直にグーグルでグーグルで検索しました。







最初の方法は工場です:







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





さて、またはこのニュアンスを隠すことができる他のパターン。







方法2-拡張メソッド:







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





驚くべきことに、それは動作します。 次の行はエラーのスローを停止します。







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





IntelliSenseがVisual Studioで、インターフェイスを明示的に実装するメソッドではなく、拡張メソッドに伝えるように、呼び出しが行われます。 そして誰も彼らに立ち向かうことを禁じません。 また、インターフェース拡張メソッドは、そのすべての実装で呼び出すことができます。







ただし、1つ注意点があります。 クラス自体の内部では、thisキーワードを介してこのメ​​ソッドにアクセスする必要があります。そうしないと、コンパイラは拡張メソッドを参照する必要があることを理解できません。







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





そのため、そうすべきではない場所にパブリックまたはパブリックがありますが、そこには害がなく、各内部インターフェイスに少し余分なコードが含まれているようです。 あなたの好みに合わせて小さな悪を選択してください。







リフレクション



リフレクションを使用してコンストラクターを見つけようとしたときに、これは痛い思いをしました。もちろん、内部クラスで内部としてマークされていました。 そして、リフレクションは公開されないものを何も提供しないことが判明しました。 そして、これは原則として論理的です。







まず、熟考します。賢い人が賢い本に書いたことを正しく覚えていれば、それはアセンブリメタデータで情報を見つけることです。 理論的には、これはあまりにも多くを与えるべきではありません(少なくともそうは思いました)。 第二に、リフレクションの主な用途は、プログラムを拡張可能にすることです。 あなたは部外者にある種のインターフェースを提供します(おそらくインターフェースの形でさえ、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の作成者は、内部クラスのメソッドがパブリックとしてマークされていれば、コンパイラーが苦痛を感じないようにし、コードをわざわざする必要がない場合、何も悪いことはないと判断しました。 インターフェイスは内部としてマークされ、それを実装するクラスは内部としてマークされ、外部からは、パブリックとしてマークされたメソッドに到達する方法がないようです。







そしてドアが開き、反射が静かに潜入します。







 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()が見つかりました

NULL! IExtensions型にメソッドSomeMethodが見つかりません

タイプAでVoid SomeMethod()が見つかりました

NULL! タイプAにメソッドOtherMethodが見つかりません

NULL! タイプBにメソッドSomeMethodが見つかりません

NULL! タイプBにメソッドOtherMethodが見つかりません



MethodInfo型のオブジェクトを使用すると、見つかったメソッドを呼び出すことができるとすぐに言わなければなりません。 つまり、反射が何かを見つけた場合、純粋に理論的にカプセル化に違反する可能性があります。 そして、何かが見つかりました。 最初に、クラスAからの同じpublic void SomeMethod()。それは、他に何を言うのか、予想されていました。 この譲歩はまだ結果をもたらすかもしれません。 次に、インターフェースIのSomeMethod()を無効にします。これはすでに興味深いものです。 どのようにロックアップしても、インターフェイスに配置された抽象メソッド(またはCLRが実際にそこに配置するもの)は実際には開いています。 したがって、別の段落で結論が下されます。







どのタイプのSystem.Typeタイプを配布するかを注意深く見てください。







しかし、これら2つの方法にはもう1つ微妙な違いがありますので、検討したいと思います。 内部インターフェイスメソッドと内部クラスのパブリックメソッドは、リフレクションを使用して見つけることができます。 合理的な人として、私は彼らがメタデータに該当すると結論付けます。 経験豊富な人として、私はこの結論を検証します。 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









クイックインスペクションにより、どのようにマークされていても、 すべてがメタデータに含まれていることがわかります。 リフレクションは、部外者が見るべきではないものをまだ慎重に隠しています。 したがって、内部インターフェイスの各メソッドの余分な5行のコードは、それほど大きな悪ではないかもしれません。 ただし、主な結論は同じままです。







どのタイプのSystem.Typeタイプを配布するかを注意深く見てください。







しかし、これはもちろん、パブリックの必要がないすべての場所でキーワードinternalを取得した後の次のレベルです。







PS



internalキーワードを使用することで最もクールなことは、アセンブリ内のどこにでもあることを知っていますか? 大きくなったら、2つ以上に分割する必要があります。 そしてその過程で、いくつかのタイプをオープンにするために休憩を取る必要があります。 そして、どのタイプがオープンになるのにふさわしいのかを正確に考える必要があります。 少なくとも簡単に。







これは、次のことを意味します。 このコードの作成方法により、新生児のアセンブリ間のアーキテクチャの境界がどのような形になるかを再考できます。 もっと美しいものはありますか?







PPS



バージョンC#7.2から、新しいプライベート保護アクセス修飾子が登場しました。 そして、私はまだそれが何で、何と一緒に食べられるのか分かりません。 実際には出会わないので。 しかし、私はコメントで知ってうれしいです。 ただし、ドキュメントからコピーアンドペーストするのではなく、実際にこのアクセス修飾子が必要になる場合があります。








All Articles