Hola lector Este artículo describe los atributos de todos los lados: desde la especificación, el significado y la definición de los atributos, creando los suyos propios y trabajando con ellos, terminando con la adición de atributos en tiempo de ejecución y los atributos existentes más útiles e interesantes. Si está interesado en el tema de los atributos en C #, bienvenido a cat.
Contenido
- Introduccion Definición y asignación de atributos
- Atributos interesantes con soporte de tiempo de ejecución. Aquí, se proporcionará una breve información sobre varios atributos, cuya existencia pocas personas conocen y menos aún quienes usan. Dado que esta es información absolutamente poco práctica, no habrá muchos despotricar (al contrario de mi pasión por el conocimiento inaplicable)
- Algunos de los atributos poco conocidos que son útiles para conocer.
- Definiendo su atributo y procesándolo. Agregar atributos en tiempo de ejecución
Introduccion
Como siempre, comience con definiciones y especificaciones. Esto ayudará a comprender y realizar los atributos en todos los niveles, lo que, a su vez, es muy útil para encontrar las aplicaciones adecuadas para ellos.
Comience definiendo metadatos.
Los metadatos son datos que describen y se refieren a los tipos definidos por
CTS . Los metadatos se almacenan de manera independiente de cualquier lenguaje de programación en particular. Por lo tanto, los metadatos proporcionan un mecanismo general para intercambiar información sobre un programa para su uso entre herramientas que lo requieren (compiladores y depuradores, así como el programa en sí), así como entre
VES . Los metadatos se incluyen en el manifiesto de ensamblado. Se pueden almacenar en un archivo
PE junto con un código
IL o en un archivo PE separado, donde solo habrá un manifiesto de ensamblado.
Un atributo es una característica de un tipo o sus miembros (u otras construcciones de lenguaje) que contiene información descriptiva. Aunque los atributos más comunes están predefinidos y tienen un formato específico en los metadatos, los atributos personalizados también se pueden agregar a los metadatos. Los atributos son conmutativos, es decir el orden de su declaración sobre el elemento no es importante
Desde un punto de vista sintáctico (en metadatos), existen los siguientes atributos- Usando sintaxis especial en IL. Por ejemplo, las palabras clave son atributos. Y para ellos hay una sintaxis especial en IL. Hay muchos de ellos; enumerar todo no tiene sentido
- Usando sintaxis generalizada. Estos incluyen los atributos de usuario y biblioteca.
- Atributos de seguridad. Estos incluyen atributos que heredan de SecurityAttribute (directa o indirectamente). Se procesan de manera especial. Hay una sintaxis especial para ellos en IL, que le permite crear xml que describe estos atributos directamente
Ejemplo
Código C # que contiene todos los tipos de atributos anteriores[StructLayout(LayoutKind.Explicit)] [Serializable] [Obsolete] [SecurityPermission(SecurityAction.Assert)] public class Sample { }
IL resultante .class public EXPLICIT ansi SERIALIZABLE beforefieldinit AttributeSamples.Sample extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.ObsoleteAttribute::.ctor() = (01 00 00 00 ) .permissionset assert = { class 'System.Security.Permissions.SecurityPermissionAttribute, System.Runtime.Extensions, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' = {}} .method public hidebysig specialname rtspecialname instance void .ctor() cil managed {/*constructor body*/} }
Como puede ver, StructLayoutAttribute tiene una sintaxis especial, ya que en IL se representa como "explícito". ObsoleteAttribute utiliza una sintaxis común: en IL comienza con ".custom". SecurityPermissionAttribute como atributo de seguridad se ha convertido en un ".permissionset afirmar".
Los atributos del usuario agregan información del usuario a los metadatos. Este mecanismo se puede utilizar para almacenar información específica de la aplicación en tiempo de compilación y acceder a ella en tiempo de ejecución o para leerla y analizarla mediante otra herramienta. Aunque cualquier tipo definido por el usuario puede usarse como un atributo, la conformidad con
CLS requiere que los atributos hereden de System.Attribute.
La CLI define previamente algunos atributos y los usa para controlar el comportamiento en tiempo de ejecución. Algunos idiomas definen atributos para representar características del lenguaje no representadas directamente en CTS.
Como ya se mencionó, los atributos se almacenan en metadatos, que, a su vez, se generan en la etapa de compilación, es decir, ingresado en el archivo PE (generalmente * .dll). Por lo tanto, puede agregar un atributo en tiempo de ejecución solo modificando el archivo ejecutable en tiempo de ejecución (pero los tiempos de los programas de cambio automático han desaparecido hace mucho tiempo). Se deduce que no se pueden agregar en la etapa de ejecución, pero esto no es del todo exacto. Si formamos nuestro ensamblaje, definimos tipos en él, entonces podemos crear un nuevo tipo en la etapa de ejecución y colgarle atributos. Entonces, formalmente, aún podemos agregar atributos en tiempo de ejecución (el ejemplo estará en la parte inferior).
Ahora un poco sobre las limitaciones
Si por alguna razón hay 2 atributos en el mismo ensamblaje con los nombres Name y NameAtribute, entonces es imposible poner el primero de ellos. Cuando se usa [Nombre] (es decir, sin sufijo), el compilador dice que ve incertidumbre. Al usar [NameAttribute] pondremos NameAttribute, que es lógico. Hay una sintaxis especial para una situación tan mística con falta de imaginación al nombrar. Para poner la primera versión sin sufijo, puede especificar el signo del perro (es decir, [Nombre] es una broma, no es necesario) antes del nombre del atributo [@Name].
Los atributos personalizados se pueden agregar a cualquier cosa que no sean atributos personalizados. Esto se refiere a metadatos, es decir Si colocamos un atributo en C # encima de la clase de atributo, en los metadatos se referirá a la clase. Pero no puede agregar un atributo a "público". Pero puede hacerlo con ensamblajes, módulos, clases, tipos de valores, enumeraciones, constructores, métodos, propiedades, campos, eventos, interfaces, parámetros, delegados, valores de retorno o parámetros generalizados. El siguiente ejemplo muestra ejemplos obvios y no muy claros de cómo puede poner un atributo en una construcción en particular.
Sintaxis de declaración de atributos using System; using System.Runtime.InteropServices; using System.Security.Permissions; using AttributeSamples; [assembly:All] [module:All] namespace AttributeSamples { [AttributeUsage(AttributeTargets.All)] public class AllAttribute : Attribute { } [All]
Los atributos tienen 2 tipos de parámetros: nombrados y posicionales. Los parámetros posicionales incluyen parámetros de constructor. Con nombre: propiedades públicas con un setter accesible. Además, estos no son solo nombres formales; todos los parámetros se pueden indicar al declarar un atributo entre paréntesis después de su nombre. Los nombrados son opcionales.
Tipos de Parámetros [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class AttrParamsAttribute : Attribute { public AttrParamsAttribute(int positional)
Los parámetros válidos (de ambos tipos) para el atributo deben ser uno de los siguientes tipos:
- bool, byte, char, double, float, int, long, short, string y más adelante primitivo, excepto decimal
- objeto
- System.Type
- enumeración
- Una matriz unidimensional de cualquiera de los tipos anteriores.
Esto se debe en gran parte al hecho de que debería ser una constante de tiempo de compilación, y los tipos anteriores pueden aceptar esta constante (al aceptar un objeto podemos pasar int). Pero por alguna razón, el argumento no puede ser de tipo ValueType, aunque esto es posible desde un punto de vista lógico.
Hay dos tipos de atributos de usuario: atributos personalizados genuinos y
pseudo-personalizados .
En el código, se ven iguales (se indican arriba de la estructura del lenguaje entre corchetes), pero se procesan de manera diferente:
- El atributo de usuario original se almacena directamente en los metadatos; Los parámetros de los atributos se almacenan tal cual. Están disponibles en tiempo de ejecución y se guardan como un conjunto de bytes (me apresuro a recordar que se conocen en tiempo de compilación)
- Se reconoce un atributo de pseudousuario porque su nombre es uno de una lista especial. En lugar de almacenar sus datos directamente en los metadatos, se analizan y se utilizan para establecer bits o campos en las tablas de metadatos, y los datos se descartan y no se pueden recibir más. Las tablas de metadatos se verifican en tiempo de ejecución más rápido que los atributos genuinos del usuario, y se requiere menos almacenamiento para almacenar información.
Los atributos de pseudousuario no son reflejo visible [Serializable] [StructLayout(LayoutKind.Explicit)] public class CustomPseudoCustom { } class Program { static void Main() { var onlyCustom = typeof(CustomPseudoCustom).GetCustomAttributes();
La mayoría de los atributos de usuario se introducen a nivel de idioma. Son almacenados y devueltos por el tiempo de ejecución, mientras que el tiempo de ejecución no sabe nada sobre el significado de estos atributos. Pero todos los atributos de pseudousuario más algunos atributos de usuario son de particular interés para los compiladores y para la CLI. Entonces pasamos a la siguiente sección.
Atributos habilitados en tiempo de ejecución
Esta sección es puramente informativa, si no hay interés en usar el tiempo de ejecución, puede desplazarse a la siguiente sección.
La siguiente tabla enumera los atributos de pseudousuario y los atributos de usuario especiales (las CLI o los compiladores los manejan de una manera especial).
Atributos de pseudousuario (no se pueden obtener a través de la reflexión).
Atributos de la CLI:
Atributos de CLS: los idiomas deben admitirlos:
Varios interesantes
Atributos útiles
Una parte integral del desarrollo de productos de software es la depuración. Y a menudo en un sistema grande y complejo, se necesitan docenas y cientos de veces para ejecutar el mismo método y monitorear el estado de los objetos. Al mismo tiempo, en un momento de 20 ya comienza a enfurecer específicamente la necesidad de expandir un objeto 400 veces para ver el valor de una variable y reiniciar el método nuevamente.
Para una depuración más silenciosa y rápida, puede usar atributos que modifiquen el comportamiento del depurador.
DebuggerDisplayAttribute indica cómo se muestra el tipo o su miembro en la ventana de variables del depurador (y no solo).
El único argumento para el constructor es una cadena con un formato de visualización. Se calculará lo que habrá entre las llaves. El formato es como una cadena interpolada, solo sin un dólar. No puede usar punteros en un valor calculado. Por cierto, si tiene una ToString anulada, entonces su valor se mostrará como si estuviera en este atributo. Si hay un ToString y un atributo, el valor se toma del atributo.
DebuggerBrowsableAttribute define cómo se muestra un campo o propiedad en la ventana de variables del depurador. Acepta un DebuggerBrowsableState, que tiene 3 opciones:
- Nunca: el campo no se muestra durante la depuración. Al expandir la jerarquía de objetos, este campo no se mostrará
- Contraído: el campo no está resuelto, pero puede expandirse. Este es el comportamiento predeterminado.
- RootHidden: el campo en sí no se muestra, pero se muestran los objetos que lo componen (para matrices y colecciones)
DebuggerTypeProxy : si el objeto se ve en el depurador cientos de veces al día, puede confundirse y pasar 3 minutos creando un objeto proxy que muestre el objeto original como debería. Por lo general, el objeto proxy para mostrar es la clase interna. En realidad, se mostrará en lugar del objeto de destino.

Otros atributos útiles
ThreadStatic : un atributo que le permite crear una variable estática propia para cada subproceso. Para hacer esto, coloque el atributo sobre el campo estático. Vale la pena recordar un matiz importante: la inicialización por un constructor estático se realizará solo una vez, y la variable cambiará en el hilo que ejecutará el constructor estático. En el resto, permanecerá en default. (PD. Si necesita este comportamiento, le aconsejo que mire hacia la clase ThreadLocal).
Un poco sobre los matices del compartimento del motor. Tanto en Linux como en Windows, hay un área de memoria local para la transmisión (
TLS y
TSD, respectivamente). Sin embargo, estas áreas en sí mismas son muy pequeñas. Por lo tanto, se crea una estructura ThreadLocalInfo, un puntero al que se coloca en TLS. En consecuencia, solo se utiliza una ranura. La estructura en sí contiene 3 campos: Thread, AppDomain, ClrTlsInfo. Estamos interesados en lo primero. Es lo que organiza el almacenamiento de las estadísticas de flujo en la memoria, utilizando ThreadLocalBlock y ThreadLocalModule para esto.
De esta manera:
- Tipos de referencia: ubicados en el montón, ThreadStaticHandleTable, que es compatible con la clase ThreadLocalBlock, mantiene enlaces a ellos.
- Estructuras: empaquetadas y almacenadas en un montón administrado, así como tipos de referencia
- Los tipos significativos primitivos se almacenan en áreas de memoria no administrada que forma parte de ThreadLocalModule
Bueno, como estamos hablando de esto, vale la pena mencionar los métodos asincrónicos. Como puede notar un lector atento, si usamos asincronía, la continuación no se ejecutará necesariamente en el mismo hilo (podemos influir en el contexto de ejecución, pero no en el hilo). En consecuencia, obtenemos una mierda si usamos ThreadLocal. En este caso, se recomienda usar AsyncLocal. Pero el artículo no trata sobre esto, así que fuimos más allá.
InternalsVisibleTo : le permite especificar el ensamblaje, que será visible para los elementos marcados como
internos . Puede parecer que si una asamblea necesita ciertos tipos y sus miembros, simplemente puede marcarlos como
públicos y no a vapor. Pero una buena arquitectura implica ocultar detalles de implementación. Sin embargo, pueden ser necesarios para algunas cosas de infraestructura, por ejemplo, proyectos de prueba. Con este atributo, puede admitir tanto la encapsulación como el porcentaje requerido de cobertura de prueba.
HandleProcessCorruptedStateExceptions : le permite asustar a los programadores tímidos y detectar excepciones de un estado dañado. Por defecto, para tales excepciones, el CLR no atrapa. En general, la mejor solución sería dejar que la aplicación se bloquee. Estas son excepciones peligrosas que indican que la memoria del proceso está dañada, por lo que usar este atributo es una muy mala idea. Pero es posible en algunos casos, para el desarrollo local será útil establecer este atributo por un tiempo. Para detectar la excepción de un estado dañado, simplemente coloque este atributo sobre el método. Y si ya ha alcanzado el uso de este atributo, se recomienda (sin embargo, como siempre) capturar alguna excepción específica.
DisablePrivateReflection : hace que todos los miembros privados del ensamblado sean inalcanzables para la reflexión. El atributo se coloca en el ensamblado.
Definiendo su atributo
No solo porque esta sección es la última. Después de todo, la mejor manera de entender en qué casos será beneficioso usar el atributo es mirar los ya usados. Es difícil decir una regla formal cuando se debe pensar en su propio atributo. A menudo se usan como información adicional sobre un tipo / miembro u otro lenguaje que es común a entidades completamente diferentes. Como ejemplo, todos los atributos utilizados para la serialización / ORM / formateo, etc. Debido a la amplia aplicación de estos mecanismos a tipos completamente diferentes, a menudo desconocidos por los desarrolladores del mecanismo correspondiente, el uso de atributos es una excelente manera de permitir al usuario proporcionar información declarativa para este mecanismo.
El uso de sus atributos se puede dividir en 2 partes:
- Crear un atributo y usarlo
- Obtener un atributo y procesarlo
Crear un atributo y usarlo
Para crear su atributo, es suficiente heredar de
System.Attribute . En este caso, es aconsejable cumplir con el estilo de nomenclatura mencionado: finalice el nombre de la clase en Attribute. Sin embargo, no habrá ningún error si omite este sufijo. Como se mencionó anteriormente, los atributos pueden tener 2 tipos de parámetros: posicionales y con nombre. La lógica de su aplicación es la misma que con las propiedades y los parámetros del constructor de la clase: los valores necesarios para crear el objeto para el que no hay un "defecto" razonable se colocan en posición (es decir, constructor). Lo que puede ser razonablemente predeterminado, que a menudo se utilizará, se distingue mejor en uno con nombre (es decir, una propiedad).
De gran importancia en la creación de un atributo es la limitación de sus lugares de aplicación. AttributeUsageAttribute se utiliza para esto. El parámetro requerido (posicional) es el AttributeTarget, que determina dónde se usa el atributo (método, ensamblaje, etc.). Los parámetros opcionales (con nombre) son:
- AllowMultiple: indica si es posible colocar más de un atributo sobre el lugar de su aplicación o no. Falso por defecto
- Heredado: determina si este atributo pertenecerá a los herederos de clases (en caso de ubicación sobre la clase base) y a los métodos anulados (en caso de ubicación sobre el método). El valor predeterminado es verdadero.
Después de eso, puede cargar los atributos con una carga útil. Un atributo es información declarativa, lo que significa que todo lo que se define en él debe describir la construcción a la que se refiere. El atributo no debe contener ninguna lógica profunda. Para el procesamiento de los atributos que defina, los servicios especiales deben ser responsables de que solo los procese. Pero el hecho de que el atributo no debería tener lógica no significa que no debería tener métodos.
Un método (función) también es información y también puede describir un diseño. Y utilizando el polimorfismo en los atributos, puede proporcionar una herramienta muy poderosa y conveniente donde el usuario puede influir tanto en la información utilizada por su herramienta como en ciertas etapas de ejecución y procesamiento.
En este caso, no necesitará producir clases, inyectar dependencias, fábricas de costos y sus interfaces que crearán estas clases. Será suficiente crear una única clase de heredero que encapsule los detalles de trabajar con el elemento con el que se relaciona. Pero, como regla, el atributo ROSO habitual con un par de propiedades es suficiente.Recuperando y procesando un atributo
El procesamiento de los atributos recibidos depende del caso específico y puede hacerse de formas completamente diferentes. Es difícil dar funciones y trucos útiles para esto.Los atributos se obtienen en tiempo de ejecución utilizando la reflexión. Hay varias formas de obtener un atributo de un elemento específico.Pero todo se origina en la interfaz ICustomAttributeProvider . Se implementa mediante tipos como Assembly, MemberInfo, Module, ParameterInfo. A su vez, los sucesores de MemberInfo son Type, EventInfo, FieldInfo, MethodBase, PropertyInfo.La interfaz tiene solo 3 funciones, y no son muy convenientes. Funcionan con matrices (incluso si sabemos que solo puede haber un atributo) y no están parametrizadas por tipo (usan objeto). Por lo tanto, rara vez tendrá que acceder directamente a las funciones de esta interfaz (nunca lo dije porque no quiero ser categórico). Para facilitar su uso, existe una clase CustomAttributeExtensions , en la que hay muchos métodos de extensión para todo tipo de tipos que realizan operaciones simples para convertir, seleccionar un valor único, etc., liberando así al desarrollador de esta necesidad. Además, estos métodos están disponibles como estáticos en la clase Attribute con la función más útil de ignorar el parámetro de herencia (para no conformistas).Las principales funciones utilizadas se enumeran a continuación. El primer parámetro que indica qué tipo extiende el método, lo omití. Además, cada vez que se especifica el parámetro heredar bool, hay una sobrecarga sin él (con el valor predeterminado de verdadero ). Este parámetro indica si los atributos de la clase principal o del método base deben tenerse en cuenta al ejecutar el método (si se usa en un método anulado). Si en el atributo herencia = flase , incluso establecerlo en verdadero no ayudará a tener en cuenta los atributos de la clase basePara mayor claridad, propongo mirar una pequeña demostración del trabajo de todas las funciones mencionadas.Valor de retorno de los métodos anteriores. [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class LogAttribute : Attribute { public string LogName { get; set; } } [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] public class SerializeAttribute : Attribute { public string SerializeName { get; set; } } [Log(LogName = "LogBase1")] [Log(LogName = "LogBase2")] [Serialize(SerializeName = "SerializeBase1")] [Serialize(SerializeName = "SerializeBase2")] public class RandomDomainEntityBase { [Log(LogName = "LogMethod1")] [Log(LogName = "LogMethod2")] [Serialize(SerializeName = "SerializeMethod1")] [Serialize(SerializeName = "SerializeMethod2")] public virtual void VirtualMethod() { throw new NotImplementedException(); } } [Log(LogName = "LogDerived1")] [Log(LogName = "LogDerived2")] [Serialize(SerializeName = "SerializeDerived1")] [Serialize(SerializeName = "SerializeDerived2")] public class RandomDomainEntityDerived : RandomDomainEntityBase { [Log(LogName = "LogOverride1")] [Log(LogName = "LogOverride2")] [Serialize(SerializeName = "SerializeOverride1")] [Serialize(SerializeName = "SerializeOverride2")] public override void VirtualMethod() { throw new NotImplementedException(); } } class Program { static void Main() { Type derivedType = typeof(RandomDomainEntityDerived); MemberInfo overrideMethod = derivedType.GetMethod("VirtualMethod");
Bueno, por interés académico, doy un ejemplo de definición de atributos en tiempo de ejecución. Este código no pretende ser el más bello y compatible.Código public class TypeCreator { private const string TypeSignature = "DynamicType"; private const string ModuleName = "DynamicModule"; private const string AssemblyName = "DynamicModule"; private readonly TypeBuilder _typeBuilder = GetTypeBuilder(); public object CreateTypeInstance() { _typeBuilder.DefineNestedType("ClassName"); CreatePropertyWithAttribute<SerializeAttribute>("PropWithAttr", typeof(int), new Type[0], new object[0]); Type newType = _typeBuilder.CreateType(); return Activator.CreateInstance(newType); } private void CreatePropertyWithAttribute<T>(string propertyName, Type propertyType, Type[] ctorTypes, object[] ctorArgs) where T : Attribute { var attributeCtor = typeof(T).GetConstructor(ctorTypes); CustomAttributeBuilder caBuilder = new CustomAttributeBuilder(attributeCtor, ctorArgs); PropertyBuilder newProperty = CreateProperty(propertyName, propertyType); newProperty.SetCustomAttribute(caBuilder); } private PropertyBuilder CreateProperty(string propertyName, Type propertyType) { FieldBuilder fieldBuilder = _typeBuilder.DefineField(propertyName, propertyType, FieldAttributes.Private); PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; MethodBuilder getter = GenerateGetter(); MethodBuilder setter = GenerateSetter(); propertyBuilder.SetGetMethod(getter); propertyBuilder.SetSetMethod(setter); return propertyBuilder; MethodBuilder GenerateGetter() { MethodBuilder getMethodBuilder = _typeBuilder.DefineMethod($"get_{propertyName}", getSetAttr, propertyType, Type.EmptyTypes); ILGenerator getterIl = getMethodBuilder.GetILGenerator(); getterIl.Emit(OpCodes.Ldarg_0); getterIl.Emit(OpCodes.Ldfld, fieldBuilder); getterIl.Emit(OpCodes.Ret); return getMethodBuilder; } MethodBuilder GenerateSetter() { MethodBuilder setMethodBuilder = _typeBuilder.DefineMethod($"set_{propertyName}", getSetAttr, null,new [] { propertyType }); ILGenerator setterIl = setMethodBuilder.GetILGenerator(); setterIl.Emit(OpCodes.Ldarg_0); setterIl.Emit(OpCodes.Ldarg_1); setterIl.Emit(OpCodes.Stfld, fieldBuilder); setterIl.Emit(OpCodes.Ret); return setMethodBuilder; } } private static TypeBuilder GetTypeBuilder() { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName); TypeBuilder typeBuilder = moduleBuilder.DefineType(TypeSignature, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,null); return typeBuilder; } } public class Program { public static void Main() { object instance = new TypeCreator().CreateTypeInstance(); IEnumerable<Attribute> attrs = instance.GetType().GetProperty("PropWithAttr").GetCustomAttributes();