¿Es la palabra clave interna de C # un olor a código?

En esta publicación, voy a mostrar por qué creo que la palabra clave interna, cuando se coloca en miembros de la clase, es un olor a código y sugiere mejores alternativas.

¿Cuál es la palabra clave interna?

En C #, la palabra clave interna se puede usar en una clase o sus miembros. Es uno de los modificadores de acceso de C #. Los tipos o miembros internos solo son accesibles dentro de los archivos del mismo ensamblado . (Documentación interna de palabras clave de C #).

¿Por qué necesitamos la palabra clave interna?

“Un uso común del acceso interno es el desarrollo basado en componentes porque permite que un grupo de componentes coopere de manera privada sin estar expuesto al resto del código de la aplicación . Por ejemplo, un marco para construir interfaces gráficas de usuario podría proporcionar clases de Control y Formulario que cooperen mediante el uso de miembros con acceso interno. Dado que estos miembros son internos, no están expuestos al código que utiliza el marco ". (Documentación interna de palabras clave de C #)

Estos son los casos de uso que vi para usar la palabra clave interna en un miembro de la clase:

  • Llame a la función privada de una clase dentro del mismo ensamblado.
  • Para probar una función privada, puede marcarla como interna y exponer la dll a la DLL de prueba a través de InternalsVisibleTo.

Ambos casos pueden verse como un olor a código, diciendo que esta función privada debería ser pública.

Veamos algunos ejemplos

He aquí un ejemplo sencillo. una función en una clase quiere acceder a una función privada de otra clase.

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

La solución es simple: simplemente marque A :: func2 como público.

Veamos un ejemplo un poco más complejo:

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

¿Cuál es el problema? simplemente marque func2 como público como lo hicimos antes.

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

¿Pero no podemos ?. B es una clase interna, por lo que no puede ser parte de la firma de una función pública de una clase pública.

Esas son las soluciones que encontré, ordenadas por facilidad:

  1. Marque la función con la palabra clave interna
public class A{ public void func1(){ } internal void func2(B b){} } internal class B{ public void func3(A a){ a.func2(this); } }

2. Crea una interfaz interna

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. Extraiga A.func2 a otra clase interna y utilícelo en lugar de 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. Desacoplar la función de las clases internas y hacerla pública. Esto depende en gran medida de lo que esté haciendo la función con sus entradas. Desacoplar las clases internas puede ser muy fácil, muy difícil e incluso imposible (sin arruinar el diseño).

Pero no tenemos clases públicas, usamos interfaces ...

Veamos un ejemplo más del mundo real:

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

Veamos cómo se adaptan las soluciones anteriores a este ejemplo:

  1. Marcar la función con Internal. esto significa que necesitará convertir a la clase para llamar a la función, por lo que esto funcionará solo si la clase A es la única que implementa la interfaz , lo que significa que IA no se burla en las pruebas y no hay otra clase de producción que implemente 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. Cree una interfaz interna que amplíe la interfaz pública.

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. Extraiga A.func2 a otra clase interna y utilícelo en lugar de A.func2.

4. Decouple the function from internal classes and add it to the public interface.

We can see that the internal keyword is the easiest solution, but there are other solutions using the traditional building blocks of OOP: classes and interfaces. We can see that the 2nd solution — adding an internal interface is not much harder than marking the function with the internal keyword.

Why not use the internal keyword?

As I showed in the previous examples, using the internal keyword is the easiest solution. But you are going to have a hard time in the future if you will need to:

  • Move the public class A to another DLL (since the internal keyword will no longer apply to the same dll)
  • Create another production class that implements IA
  • Mock IA in tests

You may think “But this is just one line of code, I or anyone else can change it easily if needed”. Now you have one line of code that looks like that:

((MyClass)a).internalFunction

but if others will need to call this function too this line will be copy-pasted around inside the DLL.

My Conclusion

I think marking a class member with the internal keyword is a code smell. In the examples I showed above it is the easiest solution, BUT can cause problems in the future. Creating an internal interface is almost as easy and more explicit.

Compare to 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?