.net - sirve - Razones convincentes para utilizar interfaces de marcador en lugar de atributos
para que sirve una clase abstracta (6)
Se ha discutido antes en el Desbordamiento de pila que deberíamos preferir los atributos a las interfaces de marcadores (interfaces sin miembros). El artículo de Diseño de Interfaz en MSDN también afirma esta recomendación:
Evite utilizar interfaces de marcador (interfaces sin miembros).
Los atributos personalizados proporcionan una forma de marcar un tipo. Para obtener más información acerca de los atributos personalizados, consulte Escritura de atributos personalizados. Se prefieren los atributos personalizados cuando puede aplazar la comprobación del atributo hasta que se ejecuta el código. Si su escenario requiere una verificación en tiempo de compilación, no puede cumplir con esta guía.
Incluso hay una regla de FxCop para hacer cumplir esta recomendación:
Evitar interfaces vacias
Las interfaces definen a los miembros que proporcionan un comportamiento o contrato de uso. La funcionalidad descrita por la interfaz puede ser adoptada por cualquier tipo, independientemente de dónde aparezca el tipo en la jerarquía de herencia. Un tipo implementa una interfaz al proporcionar implementaciones para los miembros de la interfaz. Una interfaz vacía no define ningún miembro y, como tal, no define un contrato que se pueda implementar.
Si su diseño incluye interfaces vacías que se espera que los tipos implementen, es probable que esté utilizando una interfaz como un marcador, o una forma de identificar un grupo de tipos. Si esta identificación ocurrirá en tiempo de ejecución, la forma correcta de lograrlo es usar un atributo personalizado. Utilice la presencia o ausencia del atributo, o las propiedades del atributo, para identificar los tipos de destino. Si la identificación debe ocurrir en tiempo de compilación, entonces es aceptable usar una interfaz vacía.
El artículo establece solo una razón por la que puede ignorar la advertencia: cuando necesita la identificación de tiempo de compilación para los tipos. (Esto es consistente con el artículo de Diseño de Interfaz).
Es seguro excluir una advertencia de esta regla si la interfaz se utiliza para identificar un conjunto de tipos en tiempo de compilación.
Aquí viene la pregunta real: Microsoft no cumplió con sus propias recomendaciones en el diseño de la biblioteca de clases de Framework (al menos en un par de casos): la interfaz IRequiresSessionState y la interfaz IReadOnlySessionState . Estas interfaces son utilizadas por el marco ASP.NET para verificar si debe habilitar el estado de sesión para un controlador específico o no. Obviamente, no se utiliza para la identificación de tipos en tiempo de compilación. ¿Por qué no hicieron eso? Puedo pensar en dos posibles razones:
Micro-optimización: verificar si un objeto implementa una interfaz (
obj is IReadOnlySessionState
) es más rápido que usar la reflexión para verificar un atributo (type.IsDefined(typeof(SessionStateAttribute), true)
). La diferencia es despreciable la mayor parte del tiempo, pero en realidad podría ser importante para una ruta de código crítica para el rendimiento en el tiempo de ejecución de ASP.NET. Sin embargo, existen soluciones alternativas que podrían haber utilizado, como el almacenamiento en caché del resultado para cada tipo de controlador. Lo interesante es que los servicios web ASMX (que están sujetos a características de rendimiento similares) en realidad usan la propiedadEnableSession
del atributoWebMethod
para este propósito.Las interfaces de implementación tienen más probabilidades de ser compatibles que los tipos de decoración con atributos de lenguajes .NET de terceros. Dado que ASP.NET está diseñado para ser independiente del lenguaje, y ASP.NET genera códigos para tipos (posiblemente en un idioma de terceros con la ayuda de CodeDom ) que implementan dichas interfaces basadas en el atributo
EnableSessionState
de<%@ Page %>
directiva , podría tener más sentido utilizar interfaces en lugar de atributos.
¿Cuáles son las razones persuasivas para usar interfaces de marcadores en lugar de atributos?
¿Es esto simplemente una optimización (prematura) o un pequeño error en el diseño del marco? (¿Creen que la reflexión es un "gran monstruo con ojos rojos" ?) ¿Pensamientos?
Desde el punto de vista de la codificación, creo que prefiero la sintaxis de la interfaz de marcador debido a las palabras clave integradas as
y is
. El marcado de atributos requiere un poco más de código.
[MarkedByAttribute]
public class MarkedClass : IMarkByInterface
{
}
public class MarkedByAttributeAttribute : Attribute
{
}
public interface IMarkByInterface
{
}
public static class AttributeExtension
{
public static bool HasAttibute<T>(this object obj)
{
var hasAttribute = Attribute.GetCustomAttribute(obj.GetType(), typeof(T));
return hasAttribute != null;
}
}
Y algunas pruebas para usar el código:
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class ClassMarkingTests
{
private MarkedClass _markedClass;
[TestInitialize]
public void Init()
{
_markedClass = new MarkedClass();
}
[TestMethod]
public void TestClassAttributeMarking()
{
var hasMarkerAttribute = _markedClass.HasAttibute<MarkedByAttributeAttribute>();
Assert.IsTrue(hasMarkerAttribute);
}
[TestMethod]
public void TestClassInterfaceMarking()
{
var hasMarkerInterface = _markedClass as IMarkByInterface;
Assert.IsTrue(hasMarkerInterface != null);
}
}
Generalmente evito las "interfaces de marcador" porque no te permiten desmarcar un tipo derivado. Pero aparte de eso, aquí hay algunos de los casos específicos que he visto en los que las interfaces de marcadores serían preferibles a la compatibilidad integrada con metadatos:
- Situaciones sensibles al rendimiento en tiempo de ejecución.
- Compatibilidad con idiomas que no admiten anotaciones o atributos.
- Cualquier contexto donde el código interesado no tenga acceso a los metadatos.
- Soporte para restricciones genéricas y varianza genérica (típicamente de colecciones).
Microsoft no siguió estrictamente las pautas cuando crearon .NET 1.0, porque las pautas evolucionaron junto con el marco y algunas de las reglas que no aprendieron hasta que fue demasiado tarde para cambiar la API.
IIRC, los ejemplos que mencionas pertenecen a BCL 1.0, así que eso lo explicaría.
Esto se explica en las pautas de diseño del marco .
Dicho esto, el libro también señala que "[A] la prueba de atributos es mucho más costosa que la verificación de tipo" (en una barra lateral de Rico Mariani).
Continúa diciendo que a veces se necesita la interfaz del marcador para la verificación del tiempo de compilación, lo que no es posible con un atributo. Sin embargo, encuentro el ejemplo dado en el libro (pág. 88) poco convincente, por lo que no lo repetiré aquí.
Para un tipo genérico, es posible que desee utilizar el mismo parámetro genérico en una interfaz de marcador. Esto no es alcanzable por un atributo:
interface MyInterface<T> {}
class MyClass<T, U> : MyInterface<U> {}
class OtherClass<T, U> : MyInterface<IDictionary<U, T>> {}
Este tipo de interfaz puede ser útil para asociar un tipo con otro.
Otro buen uso para una interfaz de marcador es cuando quieres crear una especie de mezcla :
interface MyMixin {}
static class MyMixinMethods {
public static void Method(this MyMixin self) {}
}
class MyClass : MyMixin {
}
El patrón de visitante acíclico también los usa. El término "interfaz degenerada" también se usa a veces.
ACTUALIZAR:
No sé si este cuenta, pero los he usado para marcar las clases para que funcione un post-compiler .
Soy fuertemente pro interfaces de marcadores. Nunca me han gustado los atributos. Los veo como algún tipo de metainformación para clases y miembros destinados, por ejemplo, para que los depuradores los vean. Al igual que las Excepciones, no deberían influir en la lógica de procesamiento normal, en mi opinión más humilde.
Desde el punto de vista del rendimiento:
Los atributos de marcador serán más lentos que las interfaces de marcador debido a la reflexión. Si no almacena en la memoria caché la reflexión, llamar a GetCustomAttributes
todo el tiempo puede ser un cuello de botella en el rendimiento. Hice una prueba comparativa de esto antes y el uso de interfaces de marcador gana en términos de rendimiento incluso cuando se utiliza la reflexión en caché.
Esto solo se aplica cuando lo usas en el código al que se llama con frecuencia.
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i5-2400 CPU 3.10GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
Frequency=3020482 Hz, Resolution=331.0730 ns, Timer=TSC
.NET Core SDK=2.1.300-rc1-008673
[Host] : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
Core : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
Job=Core Runtime=Core
Method | Mean | Error | StdDev | Rank |
--------------------------- |--------------:|-----------:|-----------:|-----:|
CastIs | 0.0000 ns | 0.0000 ns | 0.0000 ns | 1 |
CastAs | 0.0039 ns | 0.0059 ns | 0.0052 ns | 2 |
CustomAttribute | 2,466.7302 ns | 18.5357 ns | 17.3383 ns | 4 |
CustomAttributeWithCaching | 25.2832 ns | 0.5055 ns | 0.4729 ns | 3 |
Sin embargo, no es una diferencia significativa.
namespace BenchmarkStuff
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class CustomAttribute : Attribute
{
}
public interface ITest
{
}
[Custom]
public class Test : ITest
{
}
[CoreJob]
[RPlotExporter, RankColumn]
public class CastVsCustomAttributes
{
private Test testObj;
private Dictionary<Type, bool> hasCustomAttr;
[GlobalSetup]
public void Setup()
{
testObj = new Test();
hasCustomAttr = new Dictionary<Type, bool>();
}
[Benchmark]
public void CastIs()
{
if (testObj is ITest)
{
}
}
[Benchmark]
public void CastAs()
{
var itest = testObj as ITest;
if (itest != null)
{
}
}
[Benchmark]
public void CustomAttribute()
{
var customAttribute = (CustomAttribute)testObj.GetType().GetCustomAttributes(typeof(CustomAttribute), false).SingleOrDefault();
if (customAttribute != null)
{
}
}
[Benchmark]
public void CustomAttributeWithCaching()
{
var type = testObj.GetType();
bool hasAttr = false;
if (!hasCustomAttr.TryGetValue(type, out hasAttr))
{
hasCustomAttr[type] = type.CustomAttributes.SingleOrDefault(attr => attr.AttributeType == typeof(CustomAttribute)) != null;
}
if (hasAttr)
{
}
}
}
public static class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<CastVsCustomAttributes>();
}
}
}