Чи є внутрішнє ключове слово C # запахом коду?

У цьому дописі я збираюся показати, чому, на мою думку, внутрішнє ключове слово, коли його кладуть на учасників класу, є запахом коду і пропоную кращі альтернативи.

Що таке внутрішнє ключове слово?

У C # внутрішнє ключове слово можна використовувати для класу або його членів. Це один із модифікаторів доступу C #. Внутрішні типи або члени доступні лише у файлах тієї самої збірки . (Документація щодо внутрішнього ключового слова C #).

Навіщо нам внутрішнє ключове слово?

«Внутрішній доступ широко застосовується у розробці на основі компонентів, оскільки це дозволяє групі компонентів співпрацювати в приватному порядку, не піддаючись впливу інших кодів програми . Наприклад, фреймворк для побудови графічних інтерфейсів користувача може надавати класи управління та форми, які співпрацюють за допомогою членів із внутрішнім доступом. Оскільки ці члени є внутрішніми, вони не піддаються дії коду, який використовує фреймворк ”. (Документація щодо внутрішнього ключового слова C #)

Ось випадки використання, які я бачив для використання внутрішнього ключового слова для члена класу:

  • Викликати приватну функцію класу в тій самій збірці.
  • Для того, щоб протестувати приватну функцію, ви можете позначити її як внутрішню та виставити DLL на тестову DLL через InternalsVisibleTo.

Обидва випадки можна розглядати як запах коду, кажучи, що ця приватна функція повинна бути загальнодоступною.

Давайте подивимося кілька прикладів

Ось простий приклад. функція одного класу хоче отримати доступ до приватної функції іншого класу.

class A{ public void func1(){ func2(); } private void func2(){} } class B{ public void func(A a){ a.func2(); //Compilation error 'A.func2()' is inaccessible due to its protection level } }

Рішення просте - просто позначте A :: func2 як загальнодоступний.

Давайте розглянемо трохи складніший приклад:

public class A{ public void func1(){} private void func2(B b){} } internal class B{ public void func3(A a){ a.func2(this); //Compilation error 'A.func2(B)' is inaccessible due to its protection level } }

В чому проблема? просто позначте func2 як загальнодоступний, як це було раніше.

public class A{ public void func1(){ ... } public void func2(B b){ ...} // Compilation error: Inconsistent accessibility: parameter type 'B' is less accessible than method 'A.func2(B)' } internal class B{ public void func3(A a){ a.func2(this); } }

Але ми не можемо?. B - це внутрішній клас, тому він не може бути частиною підпису публічної функції публічного класу.

Це рішення, які я знайшов, упорядковані за простотою:

  1. Позначте функцію внутрішнім ключовим словом
public class A{ public void func1(){ } internal void func2(B b){} } internal class B{ public void func3(A a){ a.func2(this); } }

2. Створіть внутрішній інтерфейс

internal interface IA2{ void func2(B b); } public class A:IA2{ public void func1(){ var b = new B(); b.func3(this); } void IA2.func2(B b){} //implement IA2 explicitly because func2 can't be public } internal class B{ public void func3(A a){ ((IA2)a).func2(this); //use interface instead of class to access func2 } }

3. Витягніть A.func2 до іншого внутрішнього класу та використовуйте його замість A.func2.

internal class C{ public void func2(B b){ //extract A:func2 to here } } public class A{ public void func1(){} private void func2(B b){ new C().func2(b); } } internal class B{ public void func3(){ //a is no longer needed new C().func2(this); //use internal class instead of private function } }

4. Від’єднайте функцію від внутрішніх класів та опублікуйте її. Це багато в чому залежить від того, що робить функція зі своїми входами. роз’єднати внутрішні класи може бути дуже легко, дуже важко і навіть неможливо (не зіпсувавши дизайн).

Але у нас немає загальнодоступних класів, ми використовуємо інтерфейси ...

Давайте розглянемо ще кілька реальних прикладів:

public interface IA{ void func1(); } internal class A : IA { public void func1(){} private void func2(B b){} } internal class B{ public void func3(IA a){ a.func2(this); //Compilation error IA' does not contain a definition for 'func2' and no extension method 'func2' accepting a first argument of type 'IA' could be found } }

Давайте подивимося, як попередні рішення адаптовані до цього прикладу:

  1. Позначити функцію за допомогою Внутрішнього. це означає, що для виклику функції вам потрібно буде передати клас, тому це спрацює, лише якщо клас A є єдиним, який реалізує інтерфейс , тобто IA не висміюється в тестах і немає іншого виробничого класу, який реалізує IA .
public interface IA{ void func1(); } internal class A : IA { public void func1(){} internal void func2(B b){} } internal class B{ public void func3(IA a){ ((A)a).func2(this); //cast to A in order to accses func2 } }

2. Створіть внутрішній інтерфейс, який розширює загальнодоступний інтерфейс.

internal interface IExtendedA : IA{ void func2(B b); } public interface IA{ void func1(); } internal class A : IExtendedA { public void func1(){} public void func2(B b){} } internal class B{ public void func3(IExtendedA a){ a.func2(this); } }

3. Витягніть A.func2 до іншого внутрішнього класу та використовуйте його замість A.func2.

4. Від’єднайте функцію від внутрішніх класів та додайте її до загальнодоступного інтерфейсу.

Ми бачимо, що внутрішнє ключове слово є найпростішим рішенням , але є й інші рішення, що використовують традиційні будівельні блоки ООП: класи та інтерфейси . Ми бачимо, що друге рішення - додати внутрішній інтерфейс не набагато складніше, ніж позначити функцію внутрішнім ключовим словом.

Чому б не використовувати внутрішнє ключове слово?

Як я показав у попередніх прикладах, використання внутрішнього ключового слова є найпростішим рішенням . Але вам буде важко в майбутньому, якщо вам потрібно буде:

  • Перемістіть загальнодоступний клас A в іншу DLL (оскільки внутрішнє ключове слово більше не застосовуватиметься до тієї самої DLL)
  • Створіть ще один виробничий клас, який реалізує IA
  • Імітуйте IA в тестах

Ви можете подумати: "Але це лише один рядок коду, я або хтось інший може легко його змінити, якщо це потрібно". Тепер у вас є один рядок коду, який виглядає так:

((MyClass)a).internalFunction

але якщо іншим потрібно буде також викликати цю функцію, цей рядок буде скопійовано всередині DLL.

Мій висновок

Я думаю, що позначення члена класу внутрішнім ключовим словом є запахом коду . У прикладах, які я показав вище, це найпростіше рішення, АЛЕ може спричинити проблеми в майбутньому. Створення внутрішнього інтерфейсу майже настільки ж просто і чіткіше.

Порівняйте з C ++

The C++ “friend” keyword is similar to the C# internal keyword. It allows a class or a function to access private members of a class. The difference is it allows access to specific class or function and notall the classes in the same DLL. In my opinion, this is a better solution than the C# internal keyword.

Further Reading

Practical uses for the "internal" keyword in C#

Why does C# not provide the C++ style 'friend' keyword?