Enkapsulasi untuk samurai asli, atau nuansa yang terkait dengan kata kunci internal di C #

Prolog: internal adalah publik baru


Masing-masing dari kami memimpikan sebuah proyek di mana semuanya akan dilakukan dengan benar. Sepertinya cukup alami. Segera setelah Anda belajar tentang kemungkinan untuk menulis kode yang baik, segera setelah Anda mendengar legenda tentang kode yang sama yang dapat dengan mudah dibaca dan dimodifikasi, Anda segera menerangi dengan demikian, "Baiklah, sekarang saya akan melakukannya dengan benar, saya pintar dan saya membaca McConnell."


gambar

Proyek semacam itu terjadi dalam hidup saya. Satu lagi Dan saya melakukannya di bawah pengawasan sukarela, di mana setiap baris saya ikuti. Karena itu, saya tidak hanya ingin, tetapi saya harus melakukan semuanya dengan benar. Salah satu "benar" adalah "menghormati enkapsulasi dan mendekati maksimum, karena Anda selalu punya waktu untuk membuka, dan kemudian akan terlambat untuk menutup kembali". Dan karena itu, di mana pun saya bisa, saya mulai menggunakan pengubah akses internal daripada publik untuk kelas. Dan, tentu saja, ketika Anda mulai aktif menggunakan fitur bahasa baru untuk Anda, beberapa nuansa muncul. Saya ingin membicarakannya secara berurutan.


Bantuan dasar ofensif

Hanya untuk mengingatkan dan memberi label.


  • Majelis adalah unit penyebaran terkecil di .NET dan salah satu unit kompilasi dasar. Seperti ini, ini bisa berupa .dll atau .exe. Mereka mengatakan itu dapat dibagi menjadi beberapa file yang disebut modul.
  • pengubah akses publik, yang berarti dapat diakses oleh semua orang yang ditandai dengannya.
  • pengubah akses internal, yang artinya hanya ditandai di dalam rakitan.
  • protected - pengubah akses yang menunjukkan bahwa itu ditandai hanya tersedia untuk ahli waris kelas di mana yang ditandai berada.
  • private - pengubah akses yang menunjukkan bahwa itu ditandai hanya tersedia untuk kelas di mana ia berada. Dan tidak ada orang lain.


Tes Unit dan Build Friendly


Di C ++, ada fitur aneh seperti kelas ramah. Kelas dapat ditetapkan sebagai teman, dan kemudian batas enkapsulasi di antara mereka dihapus. Saya menduga bahwa ini bukan fitur paling aneh di C ++. Mungkin bahkan sepuluh yang paling aneh tidak termasuk. Tetapi untuk menembak diri sendiri dengan menghubungkan beberapa kelas dengan erat, entah bagaimana terlalu mudah, dan sangat sulit untuk membuat case yang cocok untuk fitur ini.


Yang lebih mengejutkan adalah mengetahui bahwa di .NET ada majelis yang ramah, semacam pemikiran ulang. Artinya, Anda dapat membuat satu rakitan melihat apa yang tersembunyi di balik kunci internal di rakitan lain. Ketika saya mengetahui hal ini, saya agak terkejut. Nah, bagaimana, mengapa? Apa gunanya Siapa yang akan mengikat erat dua majelis yang terlibat dalam pemisahan mereka? Kasus-kasus ketika dalam situasi yang tidak dapat dipahami mereka membentuk publik, kami tidak mempertimbangkan dalam artikel ini.


Dan kemudian dalam proyek yang sama, saya mulai belajar salah satu cabang dari jalur samurai sejati: pengujian unit. Dan dalam unit tes Feng Shui harus dalam majelis terpisah. Untuk Feng Shui yang sama, semua yang bisa disembunyikan di dalam rakitan, Anda harus bersembunyi di dalam rakitan. Saya menghadapi pilihan yang sangat, sangat tidak menyenangkan. Entah tes akan berbaring berdampingan dan pergi ke klien bersama dengan kode yang berguna baginya, atau semuanya akan dicakup oleh kata kunci publik, berapa lama roti telah berbaring di kelembaban.


Dan di sini, dari suatu tempat di ingatanku, sesuatu diperoleh tentang pertemuan ramah. Ternyata jika Anda memiliki majelis "YourAssemblyName", maka Anda dapat menulis seperti ini:


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

Dan majelis "YourAssemblyName.Tests" akan melihat apa yang ditandai dengan kata kunci internal di "YourAssemblyName". Baris ini dapat dimasukkan, sedikit saja, di AssemblyInfo.cs, yang VS buat secara khusus untuk menyimpan atribut tersebut.


Kembali kasar ke bantuan dasar
Di .NET, selain atribut atau kata kunci yang sudah bawaan seperti abstrak, publik, internal, statis, Anda dapat membuatnya sendiri. Dan gantungkan pada apa pun yang Anda inginkan: bidang, properti, kelas, metode, acara, dan seluruh majelis. Dalam C #, untuk ini Anda cukup menulis nama atribut dalam tanda kurung sebelum apa yang Anda tunggu. Pengecualian adalah rakitan itu sendiri, karena tidak ada indikasi langsung di mana pun dalam kode bahwa "Majelis dimulai di sini". Di sana, sebelum nama atribut, Anda perlu menambahkan rakitan:

Dengan demikian, serigala tetap penuh, domba-domba aman, segala sesuatu yang mungkin masih bersembunyi di dalam rakitan, tes unit hidup dalam rakitan terpisah, sebagaimana mestinya, dan fitur yang saya hampir tidak ingat mendapat alasan untuk menggunakannya. Mungkin satu-satunya alasan yang ada.


Saya hampir lupa satu poin penting. Tindakan atribut InternalsVisibleTo adalah satu arah.


dilindungi <internal?


Jadi situasinya: A dan B sedang duduk di atas pipa.


 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 hancur dalam proses peninjauan kode, karena tidak digunakan di luar majelis, tetapi karena alasan tertentu memungkinkan dirinya untuk memiliki pengubah akses publik, B menyebabkan kesalahan kompilasi, yang dapat menyebabkan pingsan di menit-menit pertama.


Pada dasarnya, pesan kesalahan itu logis. Accessor properti tidak dapat mengungkapkan lebih dari properti itu sendiri. Siapa pun akan bereaksi dengan pengertian jika kompiler memberikan header untuk ini:


 internal String OtherProperty { get; public set; } 

Namun klaim terhadap garis ini segera menghancurkan otak:


 internal String OtherProperty { get; protected set; } 

Saya perhatikan bahwa tidak akan ada keluhan tentang baris ini:


 internal String OtherProperty { get; private set; } 

Jika Anda tidak banyak berpikir, hierarki berikut ada di kepala Anda:


 public > internal > protected > private 

Dan hierarki ini tampaknya berfungsi. Kecuali satu tempat. Di mana internal> dilindungi. Untuk memahami esensi dari klaim kompiler, mari kita ingat pembatasan apa yang diberlakukan oleh internal dan dilindungi. internal - hanya di dalam majelis. dilindungi - hanya ahli waris. Perhatikan setiap ahli waris. Dan jika kelas B ditandai sebagai publik, maka di majelis lain Anda dapat menentukan turunannya. Dan kemudian set accessor benar-benar mendapatkan akses ke mana seluruh properti tidak memilikinya. Karena kompiler C # adalah paranoid, ia bahkan tidak bisa membiarkan kemungkinan seperti itu.


Terima kasih padanya untuk ini, tetapi kita perlu memberikan akses kepada pewaris aksesor. Dan khusus untuk kasus seperti itu, ada pengubah akses internal yang dilindungi.


Bantuan ini tidak terlalu ofensif
  • protected internal - pengubah akses yang menunjukkan bahwa yang ditandai tersedia di dalam rakitan atau ke ahli waris kelas di mana yang ditandai berada.


Jadi jika kita ingin kompiler mengizinkan kita untuk menggunakan properti ini dan mengaturnya dalam ahli waris, kita perlu melakukan ini:


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

Dan hierarki pengubah akses yang benar terlihat seperti ini:


 public > protected internal > internal/protected > private 

Antarmuka


Jadi, situasinya: A, I, B sedang duduk di atas pipa.


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

Kami duduk persis dan tidak ikut campur di luar majelis. Tetapi mereka ditolak oleh kompiler. Di sini esensi klaim jelas dari pesan kesalahan. Implementasi antarmuka harus terbuka. Bahkan jika antarmuka itu sendiri ditutup. Akan logis untuk mengikat akses ke implementasi antarmuka dengan ketersediaannya, tetapi apa yang tidak, tidak. Implementasi antarmuka harus bersifat publik.


Dan kita punya dua jalan keluar. Pertama: melalui derak dan kertakan gigi, gantung pengubah akses publik pada implementasi antarmuka. Kedua: implementasi eksplisit dari antarmuka. Ini terlihat seperti ini:


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

Harap dicatat bahwa dalam kasus kedua tidak ada pengubah akses. Kepada siapa dalam hal ini penerapan metode ini tersedia? Anggap saja tidak ada. Lebih mudah ditunjukkan dengan contoh:


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

Implementasi eksplisit dari antarmuka I berarti bahwa sampai kita secara eksplisit melemparkan variabel ke tipe I, tidak ada metode yang mengimplementasikan antarmuka ini. Menulis (b seperti saya) .Beberapa Metode () setiap kali bisa menjadi kelebihan. Seperti ((I) b) .Beberapa Metode (). Dan saya menemukan dua cara untuk mengatasi ini. Saya memikirkan satu sendiri, dan jujur ​​googled yang kedua.


Cara pertama adalah pabrik:


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

Nah, atau pola lain yang memungkinkan Anda menyembunyikan nuansa ini.


Metode dua - metode ekstensi:


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

Anehnya, itu berhasil. Baris-baris ini berhenti membuat kesalahan:


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

Setelah semua, panggilan itu datang, seperti IntelliSense memberitahu kita di Visual Studio, bukan untuk metode untuk secara eksplisit mengimplementasikan antarmuka, tetapi untuk metode ekstensi. Dan tidak ada yang melarang untuk menoleh ke mereka. Dan metode ekstensi antarmuka dapat dipanggil pada semua implementasinya.


Tetapi masih ada satu peringatan. Di dalam kelas itu sendiri, Anda perlu mengakses metode ini melalui kata kunci ini, jika tidak kompiler tidak akan mengerti bahwa kami ingin merujuk ke metode ekstensi:


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

Jadi, dan sebagainya, kita memiliki atau publik, di mana seharusnya tidak, tetapi tampaknya tidak ada salahnya, atau sedikit kode tambahan untuk setiap antarmuka internal. Pilih kejahatan yang tidak terlalu Anda sukai.


Refleksi


Saya memukul ini dengan menyakitkan ketika saya mencoba untuk menemukan konstruktor melalui refleksi, yang, tentu saja, ditandai sebagai internal di kelas internal. Dan ternyata refleksi itu tidak akan memberikan apa pun yang tidak akan dipublikasikan. Dan ini, pada prinsipnya, logis.


Pertama, refleksi, jika saya ingat dengan benar apa yang ditulis oleh orang-orang pintar dalam buku-buku pintar, ini tentang mencari informasi dalam metadata majelis. Yang, secara teori, seharusnya tidak memberi terlalu banyak (saya pikir begitu, setidaknya). Kedua, penggunaan refleksi yang utama adalah membuat program Anda bisa diperluas. Anda menyediakan semacam antarmuka untuk orang luar (mungkin bahkan dalam bentuk antarmuka, fiy-ha!). Dan mereka mengimplementasikannya dan menyediakan plugins, mods, ekstensi dalam bentuk rakitan yang dimuat saat bepergian, dari mana refleksi mendapatkannya. Dan dengan sendirinya, API Anda akan menjadi publik. Artinya, melihat internal melalui refleksi tidak secara teknis dan tidak ada gunanya dari sudut pandang praktis.


Perbarui Di sini, di komentar, ternyata refleksi memungkinkan, jika Anda secara eksplisit memintanya, untuk mencerminkan segalanya. Baik itu internal, bahkan pribadi. Jika Anda tidak menulis semacam alat analisis kode, tolong jangan lakukan itu. Teks di bawah ini masih relevan untuk kasus ketika kami mencari tipe anggota terbuka. Dan secara umum, jangan lewat komentar, ada banyak hal menarik.


Ini bisa diselesaikan dengan refleksi, tetapi mari kita kembali ke contoh sebelumnya, di mana A, I, B duduk di atas pipa:


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

Penulis kelas A memutuskan bahwa tidak ada hal buruk yang akan terjadi jika metode kelas internal ditandai sebagai publik, sehingga kompiler tidak sakit, dan sehingga tidak perlu repot kode. Antarmuka ditandai sebagai internal, kelas yang mengimplementasikannya ditandai sebagai internal, dari luar tampaknya tidak ada cara untuk sampai ke metode yang ditandai sebagai publik.


Dan kemudian pintu terbuka dan refleksi perlahan merayap masuk:


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

Pelajari kode ini, masukkan ke studio, jika diinginkan. Di sini kami mencoba menggunakan refleksi untuk menemukan semua metode dari semua jenis pipa kami (Pipa namespace). Dan inilah hasil yang diberikannya kepada kita:


Dalam tipe I kami menemukan Void SomeMethod ()
NULL! Tidak dapat menemukan metode SomeMethod dalam tipe IExtensions
Dalam tipe A kami menemukan Void SomeMethod ()
NULL! Tidak dapat menemukan metode OtherMetode dalam tipe A
NULL! Tidak dapat menemukan metode SomeMethod dalam tipe B
NULL! Tidak dapat menemukan metode OtherMetode dalam tipe B

Saya harus mengatakan segera bahwa menggunakan objek tipe MethodInfo, metode yang ditemukan dapat dipanggil. Artinya, jika refleksi menemukan sesuatu, maka enkapsulasi dapat dilanggar murni secara teoritis. Dan kami telah menemukan sesuatu. Pertama, publik yang sama membatalkan SomeMethod () dari kelas A. Diharapkan, apa lagi yang harus dikatakan. Kegemaran ini mungkin masih memiliki konsekuensi. Kedua, batal SomeMethod () dari antarmuka I. Ini sudah lebih menarik. Tidak peduli bagaimana kita mengunci diri kita, metode abstrak yang ditempatkan di antarmuka (atau apa yang sebenarnya ditempatkan CLR di sana) sebenarnya terbuka. Oleh karena itu kesimpulannya dibuat dalam paragraf terpisah:


Perhatikan dengan cermat pada siapa dan jenis System.Type apa yang Anda berikan.


Tetapi ada satu nuansa lagi dengan dua metode ini yang ditemukan, yang ingin saya pertimbangkan. Metode antarmuka internal dan metode publik dari kelas internal dapat ditemukan menggunakan refleksi. Sebagai orang yang masuk akal, saya akan menyimpulkan bahwa mereka jatuh ke dalam metadata. Sebagai orang yang berpengalaman, saya akan memverifikasi kesimpulan ini. Dan dalam ILDasm ini akan membantu kita.


Mengintip lubang kelinci di metadata pipa kami

Majelis dikumpulkan dalam rilis


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


Pandangan cepat menunjukkan bahwa semuanya masuk ke metadata, tidak peduli bagaimana itu ditandai. Refleksi masih dengan hati-hati menyembunyikan dari kita bahwa orang luar tidak seharusnya melihatnya. Jadi mungkin saja bahwa lima baris kode tambahan untuk setiap metode antarmuka internal bukanlah kejahatan besar. Namun, kesimpulan utamanya tetap sama:


Perhatikan dengan cermat pada siapa dan jenis System.Type apa yang Anda berikan.


Tapi ini, tentu saja, tingkat berikutnya, setelah aksesi kata kunci internal di semua tempat di mana tidak perlu untuk umum.


PS


Anda tahu hal paling keren tentang penggunaan kata kunci internal di mana-mana di dalam rakitan? Ketika tumbuh, Anda harus membaginya menjadi dua atau lebih. Dan dalam proses ini Anda harus istirahat untuk membuat beberapa jenis terbuka. Dan Anda harus berpikir tentang jenis apa yang layak menjadi terbuka. Setidaknya sebentar.


Ini berarti yang berikut: praktik penulisan kode ini akan membuat Anda berpikir lagi tentang apa bentuk batas arsitektural antara majelis yang baru lahir. Apa yang bisa lebih cantik?


PPS


Dimulai dengan versi C # 7.2, pengubah akses baru, yang dilindungi pribadi, telah muncul. Dan saya masih tidak tahu apa itu dan dengan apa itu dimakan. Karena tidak ditemui dalam latihan. Tapi saya akan senang mengetahui di komentar. Tapi bukan copy-paste dari dokumentasi, tetapi kasus nyata ketika pengubah akses ini mungkin diperlukan.

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


All Articles