Open Menu dzosoft
Close Menu dzosoft

   TODO SOBRE INFORMÁTICA Y TECNOLOGÍA


                             





Publish perfectly-optimized content in 1-click



 
 
 

Comenzar con C#

 

Introducción a las clases


  tipos de referencia

Un tipo que se define como una clase es un tipo de referencia. En tiempo de ejecución, cuando declaras una variable de un tipo de referencia, la variable contiene el valor nulo hasta que creas explícitamente una instancia de la clase usando el nuevo operador, o le asignas un objeto de tipo compatible. tipo que puede haberse creado en otro lugar, como se muestra en el siguiente ejemplo:
//Declarando un objeto de tipo MyClass.
MiClase myobj1 = nueva MiClase();

//Declarando otro objeto del mismo tipo, asignándole el valor del primer objeto. MiClase miobj2 = miobj1;

Cuando se crea el objeto, se asigna suficiente memoria en el montón administrado para ese objeto específico y la variable contiene solo una referencia a la ubicación de dicho objeto. La memoria utilizada por un objeto se recupera mediante la función de gestión automática de memoria del CLR, lo que se conoce como recolección de basura. Para obtener más información sobre la recolección de basura, consulte Administración automática de memoria y recolección de basura.

  Declarando clases

Las clases se declaran utilizando la palabra clave

class

seguida de un identificador único, como se muestra en el siguiente ejemplo:

//[modificador de acceso] - [clase] - [identificador]
public class Car
{
   // Los campos, propiedades, métodos y eventos van aquí...
}


Un modificador de acceso opcional precede a la palabra clave

class

. Debido a que en este caso se utiliza public, cualquiera puede crear instancias de esta clase. El nombre de la clase sigue a la palabra clave de clase. El nombre de la clase debe ser un nombre de identificador de C# válido. El resto de la definición es el cuerpo de la clase, donde se definen el comportamiento y los datos. Los campos, propiedades, métodos y eventos de una clase se denominan colectivamente miembros de la clase.


  Creando objetos

Aunque a veces se usan indistintamente, una clase y un objeto son cosas diferentes. Una clase define un tipo de objeto, pero no es un objeto en sí. Un objeto es una entidad concreta basada en una clase y, a veces, se le denomina instancia de una clase.

Los objetos se pueden crear usando la palabra clave

new

seguida del nombre de la clase, así:
Car object1 = new Car();


Cuando se crea una instancia de una clase, se devuelve al programador una referencia al objeto. En el ejemplo anterior,

object1

es una referencia a un objeto basado en

Car

. Esta referencia se refiere al nuevo objeto pero no contiene los datos del objeto en sí. De hecho, puedes crear una referencia de objeto sin crear ningún objeto:

Car object2;

No recomendamos crear referencias de objetos que no hagan referencia a un objeto porque el intento de acceder a un objeto a través de dicha referencia falla en tiempo de ejecución. Se puede hacer una referencia para hacer referencia a un objeto, ya sea creando un nuevo objeto o asignándole un objeto existente, como este:

Car object3 = new Car();
Car object4 = object3;



Este código crea dos referencias de objetos que hacen referencia al mismo objeto. Por lo tanto, cualquier cambio en el objeto realizado mediante

object3

se refleja en usos posteriores de

object4

. Debido a que se hace referencia a los objetos que se basan en clases por referencia, las clases se conocen como tipos de referencia.

  constructores e inicialización

Las secciones anteriores introdujeron la sintaxis para declarar un tipo de clase y crear una instancia de ese tipo. Cuando crea una instancia de un tipo, desea asegurarse de que sus campos y propiedades se inicialicen con valores útiles. Hay varias formas de inicializar valores:

⮚Aceptar valores predeterminados
⮚Inicializadores de campo
⮚Parámetros del constructor
⮚Inicializadores de objetos

Cada tipo de .NET tiene un valor predeterminado. Normalmente, ese valor es 0 para los tipos de números y nulo para todos los tipos de referencia. Puede confiar en ese valor predeterminado cuando sea razonable en su aplicación.

Cuando el valor predeterminado de .NET no es el valor correcto, puede establecer un valor inicial usando un inicializador de campo:

public class Container
{
    // Inicializa el campo de capacidad a un valor predeterminado de 10:
      private int _capacity = 10;
}

Puede solicitar a las personas que llaman que proporcionen un valor inicial definiendo un constructor que sea responsable de establecer ese valor inicial:
public class Container
{
    private int _capacity;

public Container(int capacity) => _capacity = capacity; }??
A partir de C# 12, puede definir un constructor primario como parte de la declaración de clase:
public class Container(int capacity) { private int _capacity = capacity; }


Agregar parámetros al nombre de la clase define el constructor principal. Esos parámetros están disponibles en el cuerpo de la clase, que incluye a sus miembros. Puede usarlos para inicializar campos o en cualquier otro lugar donde sean necesarios.

public class Person
{
    public  string LastName { get; set; }
    public  string FirstName { get; set; }
}

var p2 = new Person() { FirstName = "Carl", LastName = "John" };


  herencia de clases

Las clases soportan totalmente la herencia, una característica fundamental de la programación orientada a objetos. Cuando creas una clase, puedes heredar de cualquier otra clase que no esté definida como sellada. Otras clases pueden heredar de su clase y anular los métodos virtuales de la clase. Además, puede implementar una o más interfaces.

La herencia se logra mediante el uso de una derivación, lo que significa que una clase se declara utilizando una clase base de la cual hereda datos y comportamiento. Una clase base se especifica agregando dos puntos y el nombre de la clase base después del nombre de la clase derivada, así:

public class Manager : Employee
{
    // Employee fields, properties, methods and events are inherited
    // New Manager fields, properties, methods and events go here...
}


Cuando una declaración de clase incluye una clase base, hereda todos los miembros de la clase base excepto los constructores. Para obtener más información, consulte Herencia.


Una clase en C# solo puede heredar directamente de una clase base. Sin embargo, debido a que una clase base puede heredar de otra clase, una clase puede heredar indirectamente varias clases base. Además, una clase puede implementar directamente una o más interfaces. Para obtener más información, consulte Interfaces.

Una clase se puede declarar como abstracta. Una clase abstracta contiene métodos abstractos que tienen una definición de firma pero ninguna implementación. No se pueden crear instancias de clases abstractas. Sólo se pueden utilizar a través de clases derivadas que implementen los métodos abstractos. Por el contrario, una clase sellada no permite que otras clases deriven de ella. Para obtener más información, consulte Clases abstractas y selladas y miembros de la clase.


Las definiciones de clases se pueden dividir entre diferentes archivos fuente. Para obtener más información, ver Clases y métodos parciales.


Tipos de estructura (C# referencia)


Un tipo de estructura (o tipo de estructura) es un tipo de valor que puede encapsular datos y funciones relacionadas. Utilice la palabra clave

struct

para definir un tipo de estructura:

public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

public double X { get; } public double Y { get; }
public override string ToString() => $"({X}, {Y})"; }


Para obtener información sobre los tipos

ref struct

y

readonly ref struct

, consulte tipos de estructura de referencia artículo.

Los tipos de estructura tienen semántica de valor. Es decir, una variable de un tipo de estructura contiene una instancia del tipo. De forma predeterminada, los valores de las variables se copian en la asignación, se pasan un argumento a un método y se devuelve el resultado del método. Para las variables de tipo estructura, se copia una instancia del tipo. Para obtener más información, consulte Tipos de valores.

Normalmente, se utilizan tipos de estructura para diseñar pequeños tipos centrados en datos que proporcionan poco o ningún comportamiento. Por ejemplo, .NET utiliza tipos de estructura para representar un número (tanto entero como real), un valor booleano, un carácter Unicode o una instancia de tiempo. Si está centrado en el comportamiento de un tipo, considere definir una clase. Los tipos de clase tienen semántica de referencia. Es decir, una variable de un tipo de clase contiene una referencia a una instancia del tipo, no a la instancia en sí.

Dado que los tipos de estructura tienen una semántica de valor, le recomendamos definir tipos de estructura inmutables.

  estructura de solo lectura

Utilice el modificador

readonly

para declarar que un tipo de estructura es inmutable. Todos los miembros de datos de una estructura

readonly

deben ser de solo lectura de la siguiente manera:

Cualquier declaración de campo debe tener el modificador de solo lectura.
Cualquier propiedad, incluidas las implementadas automáticamente, debe ser de solo lectura o de solo inicio.
Eso garantiza que ningún miembro de una estructura

readonly

modifique el estado de la estructura. Eso significa que otros miembros de la instancia, excepto los constructores, son implícitamente de solo lectura.

Nota
En una estructura de sólo lectura, un miembro de datos de un tipo de referencia mutable aún
puede mutar su propio estado. Por ejemplo, no puede reemplazar una instancia de List,
pero puedes agregarle nuevos elementos.


El siguiente código define una estructura de solo lectura con establecedores de propiedades de solo inicio:

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

public double X { get; init; } public double Y { get; init; }
public override string ToString() => $"({X}, {Y})"; }


  miembros de instancia de solo lectura

También puedes usar el modificador de solo lectura para declarar que un miembro de la instancia no modifica el estado de una estructura. Si no puede declarar todo el tipo de estructura como de solo lectura, use el modificador de solo lectura para marcar los miembros de la instancia que no modifican el estado de la estructura.

Dentro de un miembro de instancia de solo lectura, no puede asignar campos de instancia de la estructura. Sin embargo, un miembro de solo lectura puede llamar a un miembro que no sea de solo lectura. En ese caso, el compilador crea una copia de la instancia de la estructura y llama al miembro que no es de solo lectura en esa copia. Como resultado, la instancia de estructura original no se modifica.

Normalmente, aplica el modificador de solo lectura a los siguientes tipos de miembros de instancia:

. métodos:

public readonly double Sum()
{
    return X + Y;
}

También puede aplicar el modificador de solo lectura a los métodos que anulan los métodos declarados en Sistema.Objeto:

cadena de anulación de solo lectura pública ToString() => $"({X}, {Y})";


. propiedades e indexadores:

private int counter;
public int Counter
{
    readonly get => counter;
    set => counter = value;
}


Si necesita aplicar el modificador de solo lectura a ambos descriptores de acceso de una propiedad o indexador, aplíquelo en la declaración de la propiedad o indexador.

Puede aplicar el modificador de solo lectura a una propiedad o indexador con un descriptor de acceso init:

public readonly double X { get; init; }


Puede aplicar el modificador de solo lectura a campos estáticos de un tipo de estructura, pero no a ningún otro miembro estático, como propiedades o métodos.

El compilador puede utilizar el modificador de solo lectura para optimizar el rendimiento.

  mutación no destructiva

A partir de C# 10, puede utilizar la expresión with para producir una copia de una instancia de tipo estructura con las propiedades y campos especificados modificados. La sintaxis del inicializador de objetos se utiliza para especificar qué miembros modificar y sus nuevos valores, como se muestra en el siguiente ejemplo.

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

public double X { get; init; } public double Y { get; init; }
public override string ToString() => $"({X}, {Y})"; }
public static void Main() { var p1 = new Coords(0, 0); Console.WriteLine(p1); // output: (0, 0)
var p2 = p1 with { X = 3 }; Console.WriteLine(p2); // output: (3, 0)
var p3 = p1 with { X = 1, Y = 4 }; Console.WriteLine(p3); // output: (1, 4) }


estructura de 4 registros


A partir de C# 10, puede definir tipos de estructura de registros. Los tipos de registros proporcionan funcionalidad integrada para encapsular datos. Puede definir los tipos

record

struct y

readonly record struct

. Una estructura de registro no puede ser una estructura de referencia. Para obtener más información y ejemplos, consulte Registros.

  matrices en línea


A partir de C# 12, puede declarar matrices en línea como tipo

struct

:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}


Una matriz en línea es una estructura que contiene un bloque contiguo de N elementos del mismo tipo. Es un código seguro equivalente a la declaración de búfer fijo disponible solo en código no seguro. Una matriz en línea es una estructura con las siguientes características:

  Contiene un solo campo.
  La estructura no especifica un diseño explícito.
Además, el compilador valida el Atributo System.Runtime.CompilerServices.InlineArrayAttribute:

La longitud debe ser mayor que cero (>0).
El tipo de destino debe ser una estructura.
En la mayoría de los casos, se puede acceder a una matriz en línea como si fuera una matriz, tanto para leer como para escribir valores. Además, puede utilizar los operadores de rango e índice.

Existen restricciones mínimas sobre el tipo de campo único. No puede ser un tipo de puntero, pero puede ser cualquier tipo de referencia o cualquier tipo de valor. Puede utilizar matrices en línea con casi cualquier estructura de datos de C#.

Las matrices en línea son una característica avanzada del lenguaje. Están pensados ​​para escenarios de alto rendimiento en los que un bloque de elementos contiguos y en línea es más rápido que otras estructuras de datos alternativas. Puede obtener más información sobre las matrices en línea en espectáculo de características

 

Inicialización de estructura y valores predeterminados.

 

Una variable de un tipo de estructura contiene directamente los datos de esa estructura. Eso crea una distinción entre una estructura no inicializada, que tiene su valor predeterminado y una estructura inicializada, que almacena los valores establecidos al construirla. Por ejemplo, considere el siguiente código:

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

public Measurement(double value, string description) { Value = value; Description = description; }
public double Value { get; init; } public string Description { get; init; }
public override string ToString() => $"{Value} ({Description})"; }
public static void Main() { var m1 = new Measurement(); Console.WriteLine(m1); // output: NaN (Undefined)
var m2 = default(Measurement); Console.WriteLine(m2); // output: 0 ()
var ms = new Measurement[2]; Console.WriteLine(string.Join(", ", ms)); // output: 0 (), 0 () }


Como muestra el ejemplo anterior, la expresión de valor predeterminado ignora un constructor sin parámetros y produce el valor predeterminado del tipo de estructura. La creación de instancias de matrices de tipo estructura también ignora un constructor sin parámetros y produce una matriz poblada con los valores predeterminados de un tipo de estructura.

La situación más común en la que ve valores predeterminados es en matrices o en otras colecciones donde el almacenamiento interno incluye bloques de variables. El siguiente ejemplo crea una matriz de 30 estructuras TemperatureRange, cada una de las cuales tiene el valor predeterminado:
// Todos los elementos tienen valores predeterminados de 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];


Todos los campos miembro de una estructura deben asignarse definitivamente cuando se crea porque los tipos de estructuras almacenan directamente sus datos. El valor predeterminado de una estructura ha asignado definitivamente todos los campos a 0. Todos los campos deben asignarse definitivamente cuando se invoca un constructor. Los campos se inicializan utilizando los siguientes mecanismos:

  Puede agregar inicializadores de campo a cualquier campo o propiedad implementada automáticamente.
  Puede inicializar cualquier campo o propiedad automática en el cuerpo del constructor.

A partir de C# 11, si no inicializa todos los campos en una estructura, el compilador agrega código al constructor que inicializa esos campos con el valor predeterminado. El compilador realiza su habitual análisis de asignación definida. A todos los campos a los que se accede antes de ser asignados, o que no se asignan definitivamente cuando el constructor termina de ejecutarse, se les asignan sus valores predeterminados antes de que se ejecute el cuerpo del constructor. Si se accede a esto antes de que se asignen todos los campos, la estructura se inicializa al valor predeterminado antes de que se ejecute el cuerpo del constructor.

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

public Measurement(double value, string description) { Value = value; Description = description; }
public Measurement(string description) { Description = description; }
public double Value { get; init; } public string Description { get; init; } = "Ordinary measurement";
public override string ToString() => $"{Value} ({Description})"; }
public static void Main() { var m1 = new Measurement(5); Console.WriteLine(m1); // output: 5 (Ordinary measurement)
var m2 = new Measurement(); Console.WriteLine(m2); // output: 0 ()
var m3 = default(Measurement); Console.WriteLine(m3); // output: 0 () }



Cada estructura tiene un constructor público sin parámetros. Si escribe un constructor sin parámetros, debe ser público. Si una estructura declara algún inicializador de campo, debe declarar explícitamente un constructor. Ese constructor no tiene por qué carecer de parámetros. Si una estructura declara un inicializador de campo pero no constructores, el compilador informa un error. Cualquier constructor declarado explícitamente (con parámetros o sin parámetros) ejecuta todos los inicializadores de campo para esa estructura. Todos los campos sin un inicializador de campo o una asignación en un constructor se establecen en el valor predeterminado. Para obtener más información, consulte la nota de propuesta de característica de constructores de estructuras sin parámetros.

A partir de C# 12, los tipos de estructura pueden definir un constructor principal como parte de su declaración. Los constructores primarios proporcionan una sintaxis concisa para los parámetros del constructor que se pueden usar en todo el cuerpo de la estructura, en cualquier declaración de miembro para esa estructura.

Si se puede acceder a todos los campos de instancia de un tipo de estructura, también puede crear una instancia sin el nuevo operador. En ese caso, debe inicializar todos los campos de la instancia antes del primer uso de la instancia. El siguiente ejemplo muestra cómo hacerlo:

public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

public static void Main() { Coords p; p.x = 3; p.y = 4; Console.WriteLine($"({p.x}, {p.y})"); // output: (3, 4) } }

En el caso de los tipos de valor integrados, utilice los literales correspondientes para especificar un valor del tipo.


 

Limitaciones con el diseño de un tipo de estructura.

 

  Las estructuras tienen la mayoría de las capacidades de un tipo de clase. Hay algunas excepciones y algunas excepciones que se han eliminado en versiones más recientes:
Un tipo de estructura no puede heredar de otra clase o tipo de estructura y no puede ser la base de una clase. Sin embargo, un tipo de estructura puede implementar interfaces.
  No se puede declarar un finalizador dentro de un tipo de estructura.
  Antes de C# 11, un constructor de un tipo de estructura debe inicializar todos los campos de instancia del tipo.

 

Pasar variables de tipo estructura por referencia.

 

Cuando pasa una variable de tipo de estructura a un método como argumento o devuelve un valor de tipo de estructura de un método, se copia toda la instancia de un tipo de estructura. El paso por valor puede afectar el rendimiento de su código en escenarios de alto rendimiento que involucran tipos de estructuras grandes. Puede evitar la copia de valores pasando una variable de tipo estructura por referencia. Utilice los modificadores de parámetros del método ref, out, in o ref readonly para indicar que un argumento debe pasarse por referencia. Utilice retornos de referencia para devolver el resultado de un método por referencia

 

restricción de estructura

 

También utiliza la palabra clave struct en la restricción de estructura para especificar que un parámetro de tipo es un tipo de valor que no acepta valores NULL. Tanto los tipos de estructura como de enumeración satisfacen la restricción de estructura.

 

Conversiones

 

Para cualquier tipo de estructura (excepto los tipos de estructura ref), existen conversiones de boxeo y unboxing hacia y desde los tipos System.ValueType y System.Object. También existen conversiones boxing y unboxing entre un tipo de estructura y cualquier interfaz que implemente.

Interfaces: definir comportamiento para múltiples tipos



Una interfaz contiene definiciones para un grupo de funcionalidades relacionadas que una clase o estructura no abstracta debe implementar. Una interfaz puede definir métodos

static

, que deben tener una implementación. Una interfaz puede definir una implementación predeterminada para los miembros. Una interfaz no puede declarar datos de instancia, como campos, propiedades implementadas automáticamente o eventos similares a propiedades.

Al utilizar interfaces, puede, por ejemplo, incluir comportamiento de múltiples fuentes en una clase. Esa capacidad es importante en C# porque el lenguaje no admite herencia múltiple de clases. Además, debe utilizar una interfaz si desea simular la herencia de estructuras, porque en realidad no pueden heredar de otra estructura o clase.

Usted define una interfaz utilizando la palabra clave

interface

como se muestra en el siguiente ejemplo.

interface IEquatable
{
    bool Equals(T obj);
}


El nombre de una interfaz debe ser un nombre de identificador de C# válido. Por convención, los nombres de las interfaces comienzan con

I

mayúscula.

Cualquier clase o estructura que implemente la interfaz

IEquatable

debe contener una definición para un método

Equals

que coincida con la firma que especifica la interfaz. Como resultado, puede contar con una clase que implemente

IEquatable

para contener un método Equals con el cual una instancia de la clase puede determinar si es igual a otra instancia de la misma clase.

La definición de

IEquatable

no proporciona una implementación para Equals. Una clase o estructura puede implementar múltiples interfaces, pero una clase solo puede heredar de una única clase.

Para obtener más información sobre las clases abstractas, consulte Clases abstractas y selladas y miembros de la clase.

Las interfaces pueden contener métodos de instancia, propiedades, eventos, indexadores o cualquier combinación de esos cuatro tipos de miembros. Las interfaces pueden contener constructores, campos, constantes u operadores estáticos. A partir de C# 11, los miembros de la interfaz que no son campos pueden ser abstractos estáticos. Una interfaz no puede contener campos de instancia, constructores de instancia ni finalizadores. Los miembros de la interfaz son públicos de forma predeterminada y puede especificar explícitamente modificadores de accesibilidad, como

público, protegido, interno, privado, interno protegido o privado protegido

. Un miembro privado debe tener una implementación predeterminada.

Para implementar un miembro de la interfaz, el miembro correspondiente de la clase de implementación debe ser público, no estático y tener el mismo nombre y firma que el miembro de la interfaz.


Una clase o estructura que implementa una interfaz debe proporcionar una implementación para todos los miembros declarados sin una implementación predeterminada proporcionada por la interfaz. Sin embargo, si una clase base implementa una interfaz, cualquier clase derivada de la clase base hereda esa implementación.

El siguiente ejemplo muestra una implementación de la interfaz

IEquatable

. La clase de implementación,

Car

, debe proporcionar una implementación del método Equals.
public class Car : IEquatable
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

// Implementation of IEquatable interface public bool Equals(Car? car) { return (this.Make, this.Model, this.Year) == (car?.Make, car?.Model, car?.Year); } }

Las propiedades y los indexadores de una clase pueden definir descriptores de acceso adicionales para una propiedad o un indexador definido en una interfaz. Por ejemplo, una interfaz podría declarar una propiedad que tenga un descriptor de acceso get. La clase que implementa la interfaz puede declarar la misma propiedad con un descriptor de acceso get y set. Sin embargo, si la propiedad o el indexador utiliza una implementación explícita, los descriptores de acceso deben coincidir. Para obtener más información sobre la implementación explícita

Las interfaces pueden heredar de una o más interfaces. La interfaz derivada hereda los miembros de sus interfaces base. Una clase que implementa una interfaz derivada debe implementar todos los miembros de la interfaz derivada, incluidos todos los miembros de las interfaces base de la interfaz derivada. Esa clase puede convertirse implícitamente a la interfaz derivada o a cualquiera de sus interfaces base. Una clase puede incluir una interfaz varias veces a través de clases base que hereda o mediante interfaces que heredan otras interfaces. Sin embargo, la clase puede proporcionar una implementación de una interfaz solo una vez y solo si la clase declara la interfaz como parte de la definición de la clase (clase ClassName: InterfaceName). Si la interfaz se hereda porque usted heredó una clase base que implementa la interfaz, la clase base proporciona la implementación de los miembros de la interfaz. Sin embargo, la clase derivada puede volver a implementar cualquier miembro de la interfaz virtual en lugar de utilizar la implementación heredada. Cuando las interfaces declaran una implementación predeterminada de un método, cualquier clase que implemente esa interfaz hereda esa implementación (debe convertir la instancia de clase al tipo de interfaz para acceder a la implementación predeterminada en el miembro de la interfaz).

Una clase base también puede implementar miembros de interfaz mediante el uso de miembros virtuales. En ese caso, una clase derivada puede cambiar el comportamiento de la interfaz anulando los miembros virtuales. Para obtener más información sobre los miembros virtuales, consulte Polimorfismo .

 

Resumen de interfaces

 

  En versiones de C# anteriores a la 8.0, una interfaz es como una clase base abstracta con solo miembros abstractos. Una clase o estructura que implementa la interfaz debe implementar a todos sus miembros.
  A partir de C# 8.0, una interfaz puede definir implementaciones predeterminadas para algunos o todos sus miembros. Una clase o estructura que implementa la interfaz no tiene que implementar miembros que tengan implementaciones predeterminadas.
  No se puede crear una instancia de una interfaz directamente. Sus miembros son implementados por cualquier clase o estructura que implemente la interfaz.
  Una clase o estructura puede implementar múltiples interfaces. Una clase puede heredar una clase base y también implementar una o más interfaces.

Introducción a los eventos


Los eventos son, como los delegados, un mecanismo de vinculación tardía. De hecho, los eventos se basan en el soporte lingüístico para los delegados.

Los eventos son una forma en que un objeto transmite (a todos los componentes interesados ​​del sistema) que algo ha sucedido. Cualquier otro componente puede suscribirse al evento y recibir una notificación cuando se genere un evento.

Probablemente hayas utilizado eventos en parte de tu programación. Muchos sistemas gráficos tienen un modelo de eventos para informar la interacción del usuario. Estos eventos informarían del movimiento del mouse, pulsaciones de botones e interacciones similares. Ese es uno de los escenarios más comunes, pero ciertamente no el único, en el que se utilizan eventos.

Puede definir eventos que deben generarse para sus clases. Una consideración importante al trabajar con eventos es que es posible que no haya ningún objeto registrado para un evento en particular. Debe escribir su código para que no genere eventos cuando no haya oyentes configurados.

La suscripción a un evento también crea un acoplamiento entre dos objetos (el origen del evento y el receptor del evento). Debe asegurarse de que el receptor de eventos cancele su suscripción al origen del evento cuando ya no esté interesado en los eventos.

 

Soporte de idiomas para eventos.

 

La sintaxis para definir eventos y suscribirse o cancelar la suscripción a eventos es una extensión de la sintaxis para delegados.

Para definir un evento, utilice la palabra clave

event

:

public event EventHandler Progress;


El tipo de evento (

EventHandler

en este ejemplo) debe ser un tipo de delegado. Hay una serie de convenciones que debes seguir al declarar un evento. Normalmente, el tipo de delegado de evento tiene un retorno nulo. Las declaraciones de eventos deben ser un verbo o una frase verbal. Utilice el tiempo pasado cuando el evento informa algo que ha sucedido. Utilice un verbo en tiempo presente (por ejemplo,

Cierre

) para informar algo que está a punto de suceder. A menudo, el uso del tiempo presente indica que su clase admite algún tipo de comportamiento de personalización. Uno de los escenarios más comunes es apoyar la cancelación. Por ejemplo, un evento de cierre puede incluir un argumento que indicaría si la operación de cierre debe continuar o no. Otros escenarios pueden permitir a los llamadores modificar el comportamiento actualizando las propiedades de los argumentos del evento. Puede generar un evento para indicar la siguiente acción propuesta que realizará un algoritmo. El controlador de eventos puede ordenar una acción diferente modificando las propiedades del argumento del evento.

Cuando desee generar el evento, llame a los controladores de eventos utilizando la sintaxis de invocación de delegado:

Progress?.Invoke(this, new FileListArgs(file));

el ?. El operador hace que sea fácil garantizar que no intente generar el evento cuando no hay suscriptores a ese evento.

Te suscribes a un evento utilizando el operador +=:

EventHandler onProgress = (sender, eventArgs) =>
    Console.WriteLine(eventArgs.FoundFile);

fileLister.Progress += onProgress;


El método del controlador normalmente tiene el prefijo "Activado" seguido del nombre del evento, como se muestra arriba.

Usted se da de baja usando el operador

-=

:

fileLister.Progress -= onProgress;

Es importante que declares una variable local para la expresión que representa el controlador de eventos. Eso garantiza que la cancelación de la suscripción elimine el controlador. Si, en cambio, utilizó el cuerpo de la expresión lambda, está intentando eliminar un controlador que nunca se ha adjuntado y que no hace nada.

Propiedades


Las propiedades son ciudadanos de primera clase en C#. El lenguaje define una sintaxis que permite a los desarrolladores escribir código que exprese con precisión su intención de diseño.

Las propiedades se comportan como campos cuando se accede a ellas. Sin embargo, a diferencia de los campos, las propiedades se implementan con descriptores de acceso que definen las declaraciones ejecutadas cuando se accede o se asigna una propiedad.

 

Sintaxis de propiedad

 
La sintaxis de las propiedades es una extensión natural de los campos. Un campo define una ubicación de almacenamiento:

public class Person
{
    public string? FirstName;

// Omitted for brevity. }


Una definición de propiedad contiene declaraciones para un descriptor de acceso get y set que recupera y asigna el valor de esa propiedad:

public class Person
{
    public string? FirstName { get; set; }

// Omitted for brevity. }


La sintaxis que se muestra arriba es la sintaxis de propiedad automática. El compilador genera la ubicación de almacenamiento para el campo que respalda la propiedad. El compilador también implementa el cuerpo de los descriptores de acceso get y set.

A veces, es necesario inicializar una propiedad con un valor distinto al predeterminado para su tipo. C# lo permite estableciendo un valor después de la llave de cierre de la propiedad. Es posible que prefiera que el valor inicial de la propiedad FirstName sea una cadena vacía en lugar de nulo. Lo especificaría como se muestra a continuación:

public class Person
{
    public string FirstName { get; set; } = string.Empty;

// Omitted for brevity. }


La inicialización específica es más útil para propiedades de solo lectura, como verá más adelante en este artículo.

También puede definir el almacenamiento usted mismo, como se muestra a continuación:

public class Person
{
    public string? FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
    private string? _firstName;

// Omitted for brevity. }


Cuando la implementación de una propiedad es una expresión única, puede utilizar miembros con cuerpo de expresión para el captador o definidor:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = value;
    }
    private string? _firstName;

// Omitted for brevity. }


Esta sintaxis simplificada se utilizará cuando corresponda a lo largo de este artículo.

La definición de propiedad que se muestra arriba es una propiedad de lectura y escritura. Observe el valor de la palabra clave en el descriptor de acceso establecido. El descriptor de acceso establecido siempre tiene un único parámetro llamado valor. El descriptor de acceso get debe devolver un valor que sea convertible al tipo de propiedad (cadena en este ejemplo).

Esos son los conceptos básicos de la sintaxis. Hay muchas variaciones diferentes que admiten varios lenguajes de diseño diferentes. Exploremos y aprendamos las opciones de sintaxis para cada uno.

 

Validación

 

Los ejemplos anteriores mostraron uno de los casos más simples de definición de propiedad: una propiedad de lectura y escritura sin validación. Al escribir el código que desea en los descriptores de acceso get y set, puede crear muchos escenarios diferentes.

Puede escribir código en el descriptor de acceso set para garantizar que los valores representados por una propiedad sean siempre válidos. Por ejemplo, supongamos que una regla para la clase Persona es que el nombre no puede estar en blanco o con espacios en blanco. Lo escribirías de la siguiente manera:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            _firstName = value;
        }
    }
    private string? _firstName;

// Omitted for brevity. }


El ejemplo anterior se puede simplificar utilizando una expresión throw como parte de la validación del definidor de propiedades:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
    }
    private string? _firstName;

// Omitted for brevity. }



El ejemplo anterior aplica la regla de que el nombre no debe estar en blanco ni con espacios en blanco. Si un desarrollador escribe

hero.FirstName = "";


Esa asignación arroja una ArgumentException. Debido a que un descriptor de acceso del conjunto de propiedades debe tener un tipo de devolución nulo, se informan errores en el descriptor de acceso del conjunto generando una excepción.

Puede extender esta misma sintaxis a cualquier cosa necesaria en su escenario. Puede comprobar las relaciones entre diferentes propiedades o validarlas frente a condiciones externas. Cualquier declaración de C# válida es válida en un descriptor de acceso de propiedad.

 

Control de acceso

 

Hasta este punto, todas las definiciones de propiedades que ha visto son propiedades de lectura/escritura con accesores públicos. Esa no es la única accesibilidad válida para las propiedades. Puede crear propiedades de solo lectura o otorgar una accesibilidad diferente al conjunto y obtener descriptores de acceso. Supongamos que su clase Persona solo debería permitir cambiar el valor de la propiedad Nombre de otros métodos en esa clase. Podrías darle al descriptor de acceso establecido accesibilidad privada en lugar de pública:

public class Person
{
    public string? FirstName { get; private set; }

// Omitido por brevedad. }


Ahora, se puede acceder a la propiedad FirstName desde cualquier código, pero solo se puede asignar desde otro código en la clase Persona.

Puede agregar cualquier modificador de acceso restrictivo al conjunto u obtener accesores. Cualquier modificador de acceso que coloque en el descriptor de acceso individual debe ser más limitado que el modificador de acceso en la definición de propiedad. Lo anterior es legal porque la propiedad FirstName es pública, pero el descriptor de acceso establecido es privado. No se podía declarar una propiedad privada con un acceso público. Las declaraciones de propiedad también pueden declararse protegidas, internas, internas protegidas o, incluso, privadas.

También es legal colocar el modificador más restrictivo en el descriptor de acceso get. Por ejemplo, podría tener una propiedad pública, pero restringir el acceso a la propiedad privada. Ese escenario rara vez se realiza en la práctica.

 

Solo lectura

 

También puede restringir las modificaciones a una propiedad para que solo se pueda establecer en un constructor. Puede modificar la clase Persona de la siguiente manera:

public class Person
{
    public Person(string firstName) => FirstName = firstName;

public string FirstName { get; }

// Omitido por brevedad. }


 

Solo inicio (Init-only)

 
El ejemplo anterior requiere que las personas que llaman utilicen el constructor que incluye el parámetro FirstName. Las personas que llaman no pueden usar inicializadores de objetos para asignar un valor a la propiedad. Para admitir inicializadores, puede convertir el descriptor de acceso set en un descriptor de acceso init, como se muestra en el siguiente código:

public class Person
{
    public Person() { }

[SetsRequiredMembers] public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
// Omitido por brevedad. }


El ejemplo anterior permite a una persona que llama crear una Persona usando el constructor predeterminado, incluso cuando ese código no establece la propiedad FirstName. A partir de C# 11, puede solicitar que las personas que llaman establezcan esa propiedad:

public class Person
{
    public Person() { }

[SetsRequiredMembers] public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
// Omitido por brevedad. }


El código anterior realiza dos adiciones a la clase Persona. Primero, la declaración de propiedad FirstName incluye el modificador requerido. Eso significa que cualquier código que cree una nueva Persona debe establecer esta propiedad. En segundo lugar, el constructor que toma un parámetro firstName tiene el atributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute. Este atributo informa al compilador que este constructor establece todos los miembros necesarios.

Las personas que llaman deben usar el constructor con SetsRequiredMembers o establecer la propiedad FirstName usando un inicializador de objeto, como se muestra en el siguiente código:

var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: se debe configurar el miembro requerido `Person.FirstName`:
//person = new VersionNinePoint2.Person();


 

Propiedades calculadas

 

Una propiedad no necesita simplemente devolver el valor de un campo miembro. Puede crear propiedades que devuelvan un valor calculado. Expandamos el objeto Persona para devolver el nombre completo, calculado concatenando el nombre y apellido:

public class Person
{
    public string? FirstName { get; set; }

public string? LastName { get; set; }
public string FullName { get { return $"{FirstName} {LastName}"; } } }

El ejemplo anterior utiliza la función de interpolación de cadenas para crear la cadena formateada para el nombre completo.

También puede utilizar un miembro con cuerpo de expresión, que proporciona una forma más concisa de crear la propiedad FullName calculada:

public class Person
{
    public string? FirstName { get; set; }

public string? LastName { get; set; }
public string FullName => $"{FirstName} {LastName}"; }

Los miembros con cuerpo de expresión utilizan la sintaxis de expresión lambda para definir métodos que contienen una única expresión. Aquí, esa expresión devuelve el nombre completo del objeto persona.

 

Propiedades evaluadas en caché

 

Puede combinar el concepto de propiedad calculada con almacenamiento y crear una propiedad evaluada en caché. Por ejemplo, podría actualizar la propiedad FullName para que el formato de la cadena solo se produzca la primera vez que se acceda a ella:


public class Person
{
    public string? FirstName { get; set; }

public string? LastName { get; set; }
private string? _fullName; public string FullName { get { if (_fullName is null) _fullName = $"{FirstName} {LastName}"; return _fullName; } } }


Sin embargo, el código anterior contiene un error. Si el código actualiza el valor de la propiedad Nombre o Apellido, el campo Nombre completo evaluado previamente no es válido. Modifica los descriptores de acceso establecidos de las propiedades Nombre y Apellido para que el campo Nombre completo se calcule nuevamente:

public class Person
{
    private string? _firstName;
    public string? FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

private string? _lastName; public string? LastName { get => _lastName; set { _lastName = value; _fullName = null; } }
private string? _fullName; public string FullName { get { if (_fullName is null) _fullName = $"{FirstName} {LastName}"; return _fullName; } } }


Esta versión final evalúa la propiedad FullName solo cuando es necesario. Si la versión calculada previamente es válida, se utiliza. Si otro cambio de estado invalida la versión calculada previamente, se volverá a calcular. Los desarrolladores que utilizan esta clase no necesitan conocer los detalles de la implementación. Ninguno de estos cambios internos afecta el uso del objeto Persona. Esa es la razón clave para usar Propiedades para exponer los miembros de datos de un objeto.

 

Adjuntar atributos a propiedades implementadas automáticamente

 

Los atributos de campo se pueden adjuntar al campo de respaldo generado por el compilador en propiedades implementadas automáticamente. Por ejemplo, considere una revisión de la clase Persona que agrega una propiedad Id de entero única. La propiedad Id se escribe mediante una propiedad implementada automáticamente, pero el diseño no requiere que se mantenga la propiedad Id. El NonSerializedAttribute solo se puede adjuntar a campos, no a propiedades. Puede adjuntar NonSerializedAttribute al campo de respaldo de la propiedad Id utilizando el especificador field: en el atributo, como se muestra en el siguiente ejemplo:

public class Person
{
    public string? FirstName { get; set; }

public string? LastName { get; set; }
[field:NonSerialized] public int Id { get; set; }
public string FullName => $"{FirstName} {LastName}"; }


Esta técnica funciona para cualquier atributo que adjunte al campo de respaldo de la propiedad implementada automáticamente.

 

Implementación de INotifyPropertyChanged

 

Un escenario final en el que necesita escribir código en un descriptor de acceso a una propiedad es admitir la interfaz INotifyPropertyChanged utilizada para notificar a los clientes de enlace de datos que un valor ha cambiado. Cuando el valor de una propiedad cambia, el objeto genera el evento INotifyPropertyChanged.PropertyChanged para indicar el cambio. Las bibliotecas de enlace de datos, a su vez, actualizan los elementos de visualización en función de ese cambio. El siguiente código muestra cómo implementaría INotifyPropertyChanged para la propiedad FirstName de esta clase de persona.
public class Person : INotifyPropertyChanged
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            if (value != _firstName)
            {
                _firstName = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(FirstName)));
            }
        }
    }
    private string? _firstName;

public event PropertyChangedEventHandler? PropertyChanged; }


El operador

?.

se denomina operador condicional nulo. Comprueba si hay una referencia nula antes de evaluar el lado derecho del operador. El resultado final es que si no hay suscriptores al evento PropertyChanged, el código para generar el evento no se ejecuta. En ese caso, arrojaría una NullReferenceException sin esta verificación. Para obtener más información, consulte eventos. Este ejemplo también utiliza el nuevo operador nameof para convertir del símbolo del nombre de propiedad a su representación de texto. El uso de nameof puede reducir los errores en los que haya escrito mal el nombre de la propiedad.

Nuevamente, implementar INotifyPropertyChanged es un ejemplo de un caso en el que puede escribir código en sus accesos para admitir los escenarios que necesita.

 

Resumiendo

 
Las propiedades son una forma de campos inteligentes en una clase u objeto. Desde fuera del objeto, aparecen como campos dentro del objeto. Sin embargo, las propiedades se pueden implementar utilizando la paleta completa de funciones de C#. Puede proporcionar validación, accesibilidad diferente, evaluación diferida o cualquier requisito que necesiten sus escenarios.

Introducción a los delegados y eventos en C#


Los delegados proporcionan un mecanismo de enlace tardío en .NET. El enlace tardío significa que se crea un algoritmo en el que la persona que llama también proporciona al menos un método que implementa parte del algoritmo.

Por ejemplo, considere ordenar una lista de estrellas en una aplicación de astronomía. Puede optar por ordenar esas estrellas por su distancia a la Tierra, la magnitud de la estrella o su brillo percibido.

En todos esos casos, el método Sort() hace esencialmente lo mismo: organiza los elementos de la lista basándose en alguna comparación. El código que compara dos estrellas es diferente para cada uno de los ordenamientos.

Este tipo de soluciones se utilizan en software desde hace medio siglo. El concepto de delegado de lenguaje C# proporciona soporte de lenguaje de primera clase y seguridad de tipos en torno al concepto.

Como verá más adelante en esta serie, el código C# que escriba para algoritmos como este es de tipo seguro. El compilador garantiza que los tipos coincidan con los argumentos y los tipos de retorno.

Los punteros de función admiten escenarios similares, en los que se necesita más control sobre la convención de llamada. El código asociado con un delegado se invoca mediante un método virtual agregado a un tipo de delegado. Utilizando punteros de función, puede especificar diferentes convenciones.

 

Objetivos de diseño lingüístico para los delegados

 

Los diseñadores del lenguaje enumeraron varios objetivos para la característica que eventualmente se convirtió en delegados.

El equipo quería una construcción de lenguaje común que pudiera usarse para cualquier algoritmo de enlace tardío. Los delegados permiten a los desarrolladores aprender un concepto y utilizar ese mismo concepto en muchos problemas de software diferentes.

En segundo lugar, el equipo quería admitir llamadas a métodos de difusión única y multidifusión. (Los delegados de multidifusión son delegados que encadenan múltiples llamadas a métodos. Verá ejemplos más adelante en esta serie).

El equipo quería que los delegados admitieran el mismo tipo de seguridad que los desarrolladores esperan de todas las construcciones de C#.

Finalmente, el equipo reconoció que un patrón de evento es un patrón específico donde los delegados, o cualquier algoritmo de enlace tardío, son útiles. El equipo quería asegurarse de que el código para los delegados pudiera proporcionar la base para el patrón de eventos .NET.

El resultado de todo ese trabajo fue el soporte para delegados y eventos en C# y .NET.

Los artículos restantes de esta serie cubrirán las características del lenguaje, el soporte de la biblioteca y los modismos comunes utilizados cuando se trabaja con delegados y eventos. Aprenderás sobre:

  La palabra clave delegada y el código que genera.

  Las funciones de la clase System.Delegate y cómo se utilizan esas funciones.
  Cómo crear delegados con seguridad de tipos.
  Cómo crear métodos que puedan invocarse a través de delegados.
  Cómo trabajar con delegados y eventos mediante expresiones lambda.
  Cómo los delegados se convierten en uno de los componentes básicos de LINQ.
  Cómo los delegados son la base del patrón de eventos de .NET y en qué se diferencian.

 

System.Delegate y la palabra clave delegada

 
  Definir tipos de delegados
Comencemos con la palabra clave 'delegar', porque eso es principalmente lo que usará cuando trabaje con delegados. El código que genera el compilador cuando usa la palabra clave delegar se asignará a llamadas a métodos que invocan miembros de las clases Delegate y MulticastDelegate.

Un tipo de delegado se define utilizando una sintaxis similar a la definición de una firma de método. Simplemente agrega la palabra clave delegada a la definición.

Sigamos usando el método List.Sort() como ejemplo. El primer paso es crear un tipo para el delegado de comparación:

// De la biblioteca .NET Core

// Definir el tipo de delegado: public delegate int Comparison(T left, T right);

El compilador genera una clase derivada de System.Delegate que coincide con la firma utilizada (en este caso, un método que devuelve un número entero y tiene dos argumentos). El tipo de ese delegado es Comparación. El tipo de delegado de comparación es un tipo genérico. Para obtener detalles sobre los genéricos, consulte aquí.

Observe que la sintaxis puede parecer como si estuviera declarando una variable, pero en realidad está declarando un tipo. Puede definir tipos de delegados dentro de clases, directamente dentro de espacios de nombres o incluso en el espacio de nombres global.

El compilador también genera controladores de adición y eliminación para este nuevo tipo, de modo que los clientes de esta clase puedan agregar y eliminar métodos de la lista de invocación de una instancia. El compilador exigirá que la firma del método que se agrega o elimina coincida con la firma utilizada al declarar el método.

  Declarar instancias de delegados

Después de definir el delegado, puede crear una instancia de ese tipo. Como todas las variables en C#, no puede declarar instancias delegadas directamente en un espacio de nombres o en el espacio de nombres global.

// dentro de una definición de clase:

// Declarar una instancia de ese tipo: public Comparison comparator;


El tipo de variable es Comparación, el tipo de delegado definido anteriormente. El nombre de la variable es comparador.

Ese fragmento de código anterior declaró una variable miembro dentro de una clase. También puede declarar variables delegadas que sean variables locales o argumentos para métodos.

  Invocar delegados

Se invocan los métodos que están en la lista de invocación de un delegado llamando a ese delegado. Dentro del método Sort(), el código llamará al método de comparación para determinar en qué orden colocar los objetos:

int result = comparator(left, right);


En la línea anterior, el código invoca el método adjunto al delegado. Trata la variable como un nombre de método y la invoca utilizando la sintaxis normal de llamada a método.

Esa línea de código hace una suposición insegura: no hay garantía de que se haya agregado un objetivo al delegado. Si no se han adjuntado objetivos, la línea anterior provocaría que se lanzara una excepción NullReferenceException. Los modismos utilizados para abordar este problema son más complicados que una simple verificación nula.

  Asignar, agregar y eliminar objetivos de invocación

Así es como se define un tipo de delegado y cómo se declaran e invocan las instancias de delegado.

Los desarrolladores que quieran utilizar el método List.Sort() deben definir un método cuya firma coincida con la definición del tipo de delegado y asignarlo al delegado utilizado por el método de clasificación. Esta asignación agrega el método a la lista de invocación de ese objeto delegado.

Supongamos que desea ordenar una lista de cadenas por su longitud. Su función de comparación podría ser la siguiente:

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);


El método se declara como método privado. Está bien. Es posible que no desee que este método forme parte de su interfaz pública. Todavía se puede utilizar como método de comparación cuando se adjunta a un delegado. El código de llamada tendrá este método adjunto a la lista de objetivos del objeto delegado y podrá acceder a él a través de ese delegado.

Creas esa relación pasando ese método al método List.Sort():

phrases.Sort(CompareLength);


Observe que se utiliza el nombre del método, sin paréntesis. El uso del método como argumento le indica al compilador que convierta la referencia del método en una referencia que pueda usarse como destino de invocación delegada y que adjunte ese método como destino de invocación.

También podrías haber sido explícito declarando una variable de tipo Comparison y haciendo una asignación:

Comparison comparer = CompareLength;
phrases.Sort(comparer);


En usos donde el método que se utiliza como destino delegado es un método pequeño, es común usar la sintaxis de expresión lambda para realizar la asignación:

Comparison comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);


El uso de expresiones lambda para objetivos delegados se trata con más detalle en una sección posterior.

El ejemplo de Sort() normalmente adjunta un único método de destino al delegado. Sin embargo, los objetos delegados admiten listas de invocación que tienen múltiples métodos de destino adjuntos a un objeto delegado.

  clases de Delegado y MulticastDelegate


El soporte de idiomas descrito anteriormente proporciona las funciones y el soporte que normalmente necesitará para trabajar con delegados. Estas características se basan en dos clases del marco .NET Core: Delegate y MulticastDelegate.

La clase System.Delegate y su única subclase directa, System.MulticastDelegate, proporcionan el marco de soporte para crear delegados, registrar métodos como destinos delegados e invocar todos los métodos que están registrados como destino delegado.

Curiosamente, las clases System.Delegate y System.MulticastDelegate no son en sí mismas tipos de delegados. Proporcionan la base para todos los tipos de delegados específicos. Ese mismo proceso de diseño del lenguaje exigía que no se pudiera declarar una clase que derive de Delegate o MulticastDelegate. Las reglas del lenguaje C# lo prohíben.

En su lugar, el compilador de C# crea instancias de una clase derivada de MulticastDelegate cuando usa la palabra clave del lenguaje C# para declarar tipos de delegados.

Este diseño tiene sus raíces en la primera versión de C# y .NET. Uno de los objetivos del equipo de diseño era garantizar que el lenguaje aplicara la seguridad de tipos al utilizar delegados. Eso significaba garantizar que se invocara a los delegados con el tipo y la cantidad de argumentos correctos. Y que cualquier tipo de devolución se indicó correctamente en el momento de la compilación. Los delegados formaron parte de la versión 1.0 .NET, anterior a los genéricos.

La mejor manera de hacer cumplir esta seguridad de tipos era que el compilador creara clases delegadas concretas que representaran la firma del método que se estaba utilizando.

Aunque no puede crear clases derivadas directamente, utilizará los métodos definidos en estas clases. Repasemos los métodos más comunes que utilizará cuando trabaje con delegados.

El primer hecho y el más importante que debe recordar es que cada delegado con el que trabaja se deriva de MulticastDelegate. Un delegado de multidifusión significa que se puede invocar más de un método objetivo cuando se invoca a través de un delegado. El diseño original consideró hacer una distinción entre delegados donde solo se podía adjuntar e invocar un método de destino, y delegados donde se podían adjuntar e invocar múltiples métodos de destino. Esa distinción resultó ser menos útil en la práctica de lo que se pensaba originalmente. Las dos clases diferentes ya fueron creadas y han estado en el marco desde su lanzamiento público inicial.

Los métodos que utilizará más con los delegados son Invoke() y BeginInvoke()/EndInvoke(). Invoke() invocará todos los métodos que se han adjuntado a una instancia de delegado particular. Como vio anteriormente, normalmente invoca a los delegados utilizando la sintaxis de llamada al método en la variable delegada. Como verá más adelante en esta serie, existen patrones que funcionan directamente con estos métodos.

Ahora que ha visto la sintaxis del lenguaje y las clases que admiten delegados, examinemos cómo se usan, crean e invocan los delegados fuertemente tipados.

 

Delegados fuertemente tipificados

 

La clase abstracta Delegate proporciona la infraestructura para la invocación y el acoplamiento flexible. Los tipos de delegados concretos se vuelven mucho más útiles al adoptar y aplicar la seguridad de tipos para los métodos que se agregan a la lista de invocación de un objeto delegado. Cuando usa la palabra clave delegar y define un tipo de delegado concreto, el compilador genera esos métodos.

En la práctica, esto conduciría a la creación de nuevos tipos de delegados siempre que necesite una firma de método diferente. Este trabajo podría volverse tedioso después de un tiempo. Cada característica nueva requiere nuevos tipos de delegados.

Afortunadamente, esto no es necesario. El marco .NET Core contiene varios tipos que puede reutilizar siempre que necesite tipos delegados. Estas son definiciones genéricas para que pueda declarar personalizaciones cuando necesite declaraciones de nuevos métodos.

El primero de estos tipos es el tipo Acción, y tiene varias variaciones:

public delegate void Action();
public delegate void Action(T arg);
public delegate void Action(T1 arg1, T2 arg2);

// Otras variaciones eliminadas por razones de brevedad.


El modificador in en el argumento de tipo genérico se trata en el artículo sobre covarianza.

Hay variaciones del delegado de acción que contienen hasta 16 argumentos, como Acción. Es importante que estas definiciones utilicen diferentes argumentos genéricos para cada uno de los argumentos delegados: eso le brinda la máxima flexibilidad. Los argumentos del método no necesitan ser del mismo tipo, pero pueden serlo.

Utilice uno de los tipos de acción para cualquier tipo de delegado que tenga un tipo de devolución nulo.

El marco también incluye varios tipos de delegados genéricos que puede usar para tipos de delegados que devuelven valores:
public delegate TResult Func();
public delegate TResult Func(T1 arg);
public delegate TResult Func(T1 arg1, T2 arg2);

// Otras variaciones eliminadas por brevedad


El modificador out en el argumento de tipo genérico del resultado se trata en el artículo sobre covarianza.

Hay variaciones del delegado Func con hasta 16 argumentos de entrada, como Func. El tipo del resultado es siempre el último parámetro de tipo en todas las declaraciones Func, por convención.

Utilice uno de los tipos Func para cualquier tipo de delegado que devuelva un valor.

También hay un tipo Predicate especializado para un delegado que devuelve una prueba sobre un único valor:

public delegate bool Predicate(T obj);


Puede observar que para cualquier tipo de Predicado, existe un tipo Func estructuralmente equivalente. Por ejemplo:

Func TestForString;
Predicate AnotherTestForString;


Se podría pensar que estos dos tipos son equivalentes. Ellos no son. Estas dos variables no se pueden utilizar indistintamente. A una variable de un tipo no se le puede asignar el otro tipo. El sistema de tipos de C# utiliza los nombres de los tipos definidos, no la estructura.

Todas estas definiciones de tipos de delegados en la biblioteca .NET Core deberían significar que no necesita definir un nuevo tipo de delegado para ninguna característica nueva que cree que requiera delegados. Estas definiciones genéricas deberían proporcionar todos los tipos de delegados que necesita en la mayoría de las situaciones. Simplemente puede crear una instancia de uno de estos tipos con los parámetros de tipo requeridos. En el caso de algoritmos que pueden volverse genéricos, estos delegados se pueden utilizar como tipos genéricos.

Esto debería ahorrar tiempo y minimizar la cantidad de tipos nuevos que necesita crear para trabajar con los delegados.

 

Patrones comunes para los delegados.

 

Un excelente ejemplo de este tipo de diseño es LINQ. El patrón de expresión de consulta LINQ se basa en delegados para todas sus funciones. Considere este sencillo ejemplo:

var smallNumbers = numbers.Where(n => n < 10);

Esto filtra la secuencia de números solo a aquellos menores que el valor 10. El método Where utiliza un delegado que determina qué elementos de una secuencia pasan el filtro. Cuando crea una consulta LINQ, proporciona la implementación del delegado para este propósito específico.

El prototipo del método Where es:

public static IEnumerable Where (this IEnumerable source, Func predicate);


Este ejemplo se repite con todos los métodos que forman parte de LINQ. Todos dependen de los delegados para el código que gestiona la consulta específica. Este patrón de diseño de API es poderoso para aprender y comprender.
Este sencillo ejemplo ilustra cómo los delegados requieren muy poco acoplamiento entre componentes. No es necesario crear una clase que derive de una clase base particular. No es necesario implementar una interfaz específica. El único requisito es proporcionar la implementación de un método que sea fundamental para la tarea en cuestión.

 

Construya sus propios componentes con los delegados

 

Basándonos en ese ejemplo, crearemos un componente utilizando un diseño que dependa de delegados.

Definamos un componente que podría usarse para registrar mensajes en un sistema grande. Los componentes de la biblioteca podrían usarse en muchos entornos diferentes, en múltiples plataformas diferentes. Hay muchas características comunes en el componente que gestiona los registros. Deberá aceptar mensajes de cualquier componente del sistema. Esos mensajes tendrán diferentes prioridades, que el componente central puede gestionar. Los mensajes deben tener marcas de tiempo en su formato archivado final. Para escenarios más avanzados, puede filtrar mensajes por componente de origen.

Hay un aspecto de la función que cambiará con frecuencia: dónde se escriben los mensajes. En algunos entornos, es posible que se escriban en la consola de errores. En otros, un archivo. Otras posibilidades incluyen almacenamiento de bases de datos, registros de eventos del sistema operativo u otro almacenamiento de documentos.

También hay combinaciones de resultados que podrían usarse en diferentes escenarios. Es posible que desee escribir mensajes en la consola y en un archivo.

Un diseño basado en delegados proporcionará una gran flexibilidad y facilitará el soporte de mecanismos de almacenamiento que puedan agregarse en el futuro.

Según este diseño, el componente de registro principal puede ser una clase no virtual e incluso sellada. Puede conectar cualquier conjunto de delegados para escribir los mensajes en diferentes medios de almacenamiento. La compatibilidad integrada con delegados de multidifusión facilita la compatibilidad con escenarios en los que los mensajes deben escribirse en varias ubicaciones (un archivo y una consola).

 

Una primera implementación

 

public static class Logger
{
    public static Action? WriteMessage;

public static void LogMessage(string msg) { if (WriteMessage is not null) WriteMessage(msg); } }

La clase estática anterior es lo más simple que puede funcionar. Necesitamos escribir la implementación única para el método que escribe mensajes en la consola:

public static class LoggingMethods
{
    public static void LogToConsole(string message)
    {
        Console.Error.WriteLine(message);
    }
}


Finalmente, necesitas conectar el delegado adjuntándolo al delegado WriteMessage declarado en el registrador:

Logger.WriteMessage += LoggingMethods.LogToConsole;


 

Prácticas

 

Nuestro ejemplo hasta ahora es bastante simple, pero aún demuestra algunas de las pautas importantes para diseños que involucran delegados.

El uso de los tipos de delegados definidos en el marco central facilita a los usuarios trabajar con los delegados. No es necesario definir nuevos tipos y los desarrolladores que utilizan su biblioteca no necesitan aprender nuevos tipos de delegados especializados.

Las interfaces utilizadas son lo más mínimas y flexibles posible: para crear un nuevo registrador de salida, debe crear un método. Ese método puede ser un método estático o un método de instancia. Puede tener cualquier acceso.

 

Formatear salida

 

Hagamos esta primera versión un poco más sólida y luego comencemos a crear otros mecanismos de registro.

A continuación, agreguemos algunos argumentos al método LogMessage() para que su clase de registro cree mensajes más estructurados:

public enum Severity
{
    Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}

public static class Logger
{
    public static Action? WriteMessage;

public static void LogMessage(Severity s, string component, string msg) { var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}"; if (WriteMessage is not null) WriteMessage(outputMsg); } }

A continuación, usemos ese argumento de Gravedad para filtrar los mensajes que se envían a la salida del registro.
public static class Logger
{
    public static Action? WriteMessage;

public static Severity LogLevel { get; set; } = Severity.Warning;
public static void LogMessage(Severity s, string component, string msg) { if (s < LogLevel) return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}"; if (WriteMessage is not null) WriteMessage(outputMsg); } }


 

Prácticas

 
Ha agregado nuevas funciones a la infraestructura de registro. Debido a que el componente del registrador está muy poco acoplado a cualquier mecanismo de salida, estas nuevas características se pueden agregar sin impacto en el código que implementa el delegado del registrador.

A medida que sigas construyendo esto, verás más ejemplos de cómo este acoplamiento flexible permite una mayor flexibilidad a la hora de actualizar partes del sitio sin realizar cambios en otras ubicaciones. De hecho, en una aplicación más grande, las clases de salida del registrador podrían estar en un ensamblado diferente y ni siquiera sería necesario reconstruirlas.

 

Construir un segundo motor de salida

 
El componente Log va bien. Agreguemos un motor de salida más que registre mensajes en un archivo. Este será un motor de salida un poco más complicado. Será una clase que encapsule las operaciones del archivo y garantice que el archivo siempre esté cerrado después de cada escritura. Eso garantiza que todos los datos se vacíen al disco después de que se genere cada mensaje.

Aquí está ese registrador basado en archivos:

public class FileLogger
{
    private readonly string logPath;
    public FileLogger(string path)
    {
        logPath = path;
        Logger.WriteMessage += LogMessage;
    }

public void DetachLog() => Logger.WriteMessage -= LogMessage; // make sure this can't throw. private void LogMessage(string msg) { try { using (var log = File.AppendText(logPath)) { log.WriteLine(msg); log.Flush(); } } catch (Exception) { // Hmm. We caught an exception while // logging. We can't really log the // problem (since it's the log that's failing). // So, while normally, catching an exception // and doing nothing isn't wise, it's really the // only reasonable option here. } } }


Una vez que haya creado esta clase, puede crear una instancia y adjunta su método LogMessage al componente Logger:

var file = new FileLogger("log.txt");

Estos dos no son mutuamente excluyentes. Podrías adjuntar ambos métodos de registro y generar mensajes a la consola y un archivo:

var fileOutput = nuevo FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods es la clase estática que utilizamos anteriormente


Posteriormente, incluso en la misma aplicación, podrás eliminar a uno de los delegados sin ningún otro problema para el sistema:


Logger.WriteMessage -= LoggingMethods.LogToConsole;


 

Prácticas

 

Ahora ha agregado un segundo controlador de salida para el subsistema de registro. Este necesita un poco más de infraestructura para soportar correctamente el sistema de archivos. El delegado es un método de instancia. También es un método privado. No hay necesidad de mayor accesibilidad porque la infraestructura de delegados puede conectar a los delegados.

En segundo lugar, el diseño basado en delegados permite múltiples métodos de salida sin ningún código adicional. No es necesario crear ninguna infraestructura adicional para admitir múltiples métodos de salida. Simplemente se convierten en un método más en la lista de invocación.

Preste especial atención al código en el método de salida del registro de archivos. Está codificado para garantizar que no genere ninguna excepción. Si bien esto no siempre es estrictamente necesario, suele ser una buena práctica. Si cualquiera de los métodos delegados genera una excepción, los delegados restantes que estén en la invocación no serán invocados.

Como última nota, el registrador de archivos debe administrar sus recursos abriendo y cerrando el archivo en cada mensaje de registro. Puede optar por mantener el archivo abierto e implementar IDisposable para cerrar el archivo cuando haya terminado. Cualquiera de los métodos tiene sus ventajas y desventajas. Ambos crean un poco más de acoplamiento entre las clases.

No sería necesario actualizar ningún código de la clase Logger para admitir cualquiera de los escenarios.

 

Manejar delegados nulos

 

Finalmente, actualicemos el método LogMessage para que sea robusto en aquellos casos en los que no se selecciona ningún mecanismo de salida. La implementación actual generará una excepción NullReferenceException cuando el delegado WriteMessage no tenga una lista de invocación adjunta. Es posible que prefiera un diseño que continúe silenciosamente cuando no se hayan adjuntado métodos. Esto es fácil usando el operador condicional nulo, combinado con el método Delegate.Invoke():

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}


El operador condicional nulo (?.) produce un cortocircuito cuando el operando izquierdo (WriteMessage en este caso) es nulo, lo que significa que no se intenta registrar un mensaje.

No encontrará el método Invoke() listado en la documentación de System.Delegate o System.MulticastDelegate. El compilador genera un método Invoke de tipo seguro para cualquier tipo de delegado declarado. En este ejemplo, eso significa que Invoke toma un único argumento de cadena y tiene un tipo de retorno nulo.


 

Resumen de Prácticas

 
Ha visto los inicios de un componente de registro que podría ampliarse con otros escritores y otras características. Al utilizar delegados en el diseño, estos diferentes componentes están débilmente acoplados. Esto proporciona varias ventajas. Es fácil crear nuevos mecanismos de salida y adjuntarlos al sistema. Estos otros mecanismos sólo necesitan un método: el método que escribe el mensaje de registro. Es un diseño resistente cuando se agregan nuevas funciones. El contrato requerido para cualquier escritor es implementar un método. Ese método podría ser un método estático o de instancia. Puede ser acceso público, privado o cualquier otro acceso legal.

La clase Logger puede realizar cualquier cantidad de mejoras o cambios sin introducir cambios importantes. Como cualquier clase, no puede modificar la API pública sin correr el riesgo de realizar cambios importantes. Pero, debido a que el acoplamiento entre el registrador y cualquier motor de salida se realiza solo a través del delegado, no intervienen otros tipos (como interfaces o clases base). El acoplamiento es lo más pequeño posible.


Operadores y expresiones de C#


C# proporciona varios operadores. Muchos de ellos son compatibles con los tipos integrados y le permiten realizar operaciones básicas con valores de esos tipos. Esos operadores incluyen los siguientes grupos:

  Operadores aritméticos que realizan operaciones aritméticas con operandos numéricos
  operadores de comparación que comparan operandos numéricos
  operadores lógicos booleanos que realizan operaciones lógicas con operandos booleanos
  operadores bit a bit y de desplazamiento que realizan operaciones bit a bit o de desplazamiento con operandos de tipo integral
  operadores de igualdad que comprueban si sus operandos son iguales o no

Normalmente, puede sobrecargar esos operadores, es decir, especificar el comportamiento del operador para los operandos de un tipo definido por el usuario.

Las expresiones de C# más simples son literales (por ejemplo, números enteros y reales) y nombres de variables. Puede combinarlos en expresiones complejas mediante el uso de operadores. La precedencia y asociatividad de los operadores determinan el orden en el que se realizan las operaciones en una expresión. Puede utilizar paréntesis para cambiar el orden de evaluación impuesto por la precedencia y asociatividad de los operadores.

En el siguiente código, hay ejemplos de expresiones en el lado derecho de las asignaciones:

int a, b, c;
a = 7;
b = a;
c = b++;
b = a + b * c;
c = a >= 100 ? b : c / 10;
a = (int)Math.Sqrt(b * b + c * c);

string s = "String literal"; char l = s[s.Length - 1];
List numbers = [..collection]; b = numbers.FindLast(n => n > 1);


Normalmente, una expresión produce un resultado y puede incluirse en otra expresión. Una llamada a un método nulo es un ejemplo de una expresión que no produce un resultado. Sólo se puede utilizar como una declaración, como muestra el siguiente ejemplo:

Console.WriteLine("¡Hola mundo!");


A continuación se muestran algunos otros tipos de expresiones que proporciona C#:

  Expresiones de cadena interpoladas que proporcionan una sintaxis conveniente para crear cadenas formateadas:

var r = 2.3;
var message = $"The area of a circle with radius {r} is {Math.PI * r * r:F3}.";
Console.WriteLine(message);
// Producción:
// El área de un círculo con radio 2,3 es 16,619.

  expresiones Lambda que te permiten crear funciones anónimas:

int[] numeros = [2, 3, 4, 5];
var maximoCuadrado = numeros.Max(x => x * x);
Console.WriteLine(maximoCuadrado);
// Producción:
// 25

  expresiones de consulta que le permiten utilizar capacidades de consulta directamente en C#:

int[] scores = [90, 97, 78, 68, 85];
IEnumerable highScoresQuery =
    from score in scores
    where score > 80
    orderby score descending
    select score;
Console.WriteLine(string.Join(" ", highScoresQuery));
// Producción:
// 97 90 85


Puede utilizar una definición de cuerpo de expresión para proporcionar una definición concisa de un método, constructor, propiedad, indexador o finalizador.

 

Precedencia del operador

 

En una expresión con múltiples operadores, los operadores con mayor precedencia se evalúan antes que los operadores con menor precedencia. En el siguiente ejemplo, la multiplicación se realiza primero porque tiene mayor prioridad que la suma:

var a = 2 + 2 * 2;
Consola.WriteLine(a); // salida: 6

Utilice paréntesis para cambiar el orden de evaluación impuesto por la precedencia del operador:

var a = (2 + 2) * 2;
Consola.WriteLine(a); // salida: 8


La siguiente tabla enumera los operadores de C# desde la prioridad más alta hasta la más baja. Los operadores dentro de cada fila tienen la misma prioridad.


OperadoresCategoría o nombre
x.y, f(x), a[i], x?.y, x?[y], x++, x--, x!, nuevo, tipo de, marcado, sin marcar, predeterminado, nombre de, delegado, tamaño de, stackalloc, x->y Primario
+x, -x, !x, ~x, ++x, --x, ^x, (T)x, await, &x, *x, verdadero y falso Unario
x..y Rango
cambiar, con cambiar y con expresiones
x * y, x / y, x % y Multiplicativo
x + y, x – y Aditivo
x << y, x >> y, x >>> y Mayús
x < y, x > y, x <= y, x >= y, es, como Prueba relacional y de tipo
x == y, x != y Igualdad
x & y Y lógico booleano o Y lógico bit a bit
x ^ y XOR lógico booleano o XOR lógico bit a bit
x | y O lógico booleano o O lógico bit a bit
x && y Y condicional
x || y Condicional O
x ?? y Operador coalescente nulo
c ? t : f Operador condicional
x = y, x += y, x -= y, x *= y, x /= y, x %= y, x &= y, x |= y, x ^= y, x <<= y, x >>= y, x >>>= y, x ??= y, => Asignación y declaración lambda
 

Asociatividad de operadores

 

Cuando los operadores tienen la misma precedencia, la asociatividad de los operadores determina el orden en el que se realizan las operaciones:

  Los operadores asociativos por izquierda se evalúan en orden de izquierda a derecha. Excepto los operadores de asignación y los operadores de fusión nula, todos los operadores binarios son asociativos por la izquierda. Por ejemplo, a + b - c se evalúa como (a + b) - c.
  Los operadores asociativos por la derecha se evalúan en orden de derecha a izquierda. Los operadores de asignación, los operadores de fusión nula, las lambdas y el operador condicional ?: son asociativos por la derecha. Por ejemplo, x = y = z se evalúa como x = (y = z).
int a = 13/5/2;
int b = 13 / (5 / 2);
Console.WriteLine($"a = {a}, b = {b}"); // salida: a = 1, b = 6


 

Evaluación de operandos

 

Sin relación con la precedencia y la asociatividad de los operadores, los operandos en una expresión se evalúan de izquierda a derecha. Los siguientes ejemplos demuestran el orden en el que se evalúan los operadores y operandos:

Expresión Orden de evaluación
a + b a, b, +
a + b * c a, b, c, *, +
a / b + c * d a, b, /, c, d, *, +
a / (b + c) * d a, b, c, +, /, d, *
Normalmente, se evalúan todos los operandos del operador. Sin embargo, algunos operadores evalúan operandos de forma condicional. Es decir, el valor del operando más a la izquierda de dicho operador define si (o cuáles) otros operandos deben evaluarse. Estos operadores son los operadores lógicos condicionales AND (&&) y OR (||), los operadores de fusión nula ?? y ??=, los operadores condicionales nulos ?. y ?[], y el operador condicional ?:.

Declaraciones

Las acciones que realiza un programa se expresan en declaraciones. Las acciones comunes incluyen declarar variables, asignar valores, llamar a métodos, recorrer colecciones y bifurcarse a uno u otro bloque de código, dependiendo de una condición determinada. El orden en el que se ejecutan las sentencias en un programa se denomina flujo de control o flujo de ejecución. El flujo de control puede variar cada vez que se ejecuta un programa, dependiendo de cómo reacciona el programa a la entrada que recibe en tiempo de ejecución.

Una declaración puede consistir en una sola línea de código que termina en punto y coma, o una serie de declaraciones de una sola línea en un bloque. Un bloque de declaración está entre corchetes {} y puede contener bloques anidados. El siguiente código muestra dos ejemplos de declaraciones de una sola línea y un bloque de declaraciones de varias líneas:

  public static void Main()
    {
        // Declaración.
       int counter

// Sentencia de asignación. counter = 1;
// ¡Error! Esta es una expresión, no una declaración de expresión. // counter + 1;
// Las declaraciones de declaración con inicializadores son funcionalmente // equivalente a una declaración seguida de una declaración de asignación: int[] radii = [15, 32, 108, 74, 9]; // Declare and initialize an array. const double pi = 3.14159; // Declare and initialize constant.
// bloque de declaraciones foreach que contiene varias declaraciones. foreach (int radius in radii) { // Declaración de declaración con inicializador. double circumference = pi * (2 * radius);
// Declaración de expresión (invocación de método). una sola linea // la declaración puede abarcar varias líneas de texto porque hay saltos de línea // se tratan como espacios en blanco, que el compilador ignora. System.Console.WriteLine("El radio del círculo #{0} es {1}. Circunferencia = {2:N2}", contador, radio, circunferencia);
// Declaración de expresión (incremento de sufijo). counter++; } // Fin del bloque de declaración foreach } // Fin del cuerpo del método principal. } // Fin de la clase SimpleStatements.

   Producción:
    Radio del círculo #1 = 15. Circunferencia = 94,25
    Radio del círculo #2 = 32. Circunferencia = 201,06
    Radio del círculo #3 = 108. Circunferencia = 678,58
    Radio del círculo #4 = 74. Circunferencia = 464,96
    Radio del círculo #5 = 9. Circunferencia = 56,55


 

Tipos de declaraciones

 

La siguiente tabla enumera los distintos tipos de declaraciones en C# y sus palabras clave asociadas, con vínculos a temas que incluyen más información:

Categoría Palabras clave/notas de C#
Declaraciones de declaración Una declaración de declaración introduce una nueva variable o constante. Una declaración de variable puede opcionalmente asignar un valor a la variable. En una declaración constante, la asignación es obligatoria.
Declaraciones de expresión Las declaraciones de expresión que calculan un valor deben almacenar el valor en una variable.
Declaraciones de selección Las declaraciones de selección le permiten pasar a diferentes secciones de código, dependiendo de una o más condiciones especificadas. Para obtener más información, consulte los siguientes temas:
si
cambiar
Declaraciones de iteración Las declaraciones de iteración le permiten recorrer colecciones como matrices o realizar el mismo conjunto de declaraciones repetidamente hasta que se cumpla una condición específica. Para obtener más información, consulte los siguientes temas:
hacer
para
foreach
mientras
Declaraciones de salto Las declaraciones de salto transfieren el control a otra sección de código. Para obtener más información, consulte los siguientes temas:
descanso
continuar
ir a
volver
rendimiento

Declaraciones de manejo de excepciones Las declaraciones de manejo de excepciones le permiten recuperarse con gracia de condiciones excepcionales que ocurren en tiempo de ejecución. Para obtener más información, consulte los siguientes temas:
tirar
intento-catch
intenta-finalmente
intenta-capturar-finalmente
marcado y no marcado Las declaraciones marcadas y no marcadas le permiten especificar si las operaciones numéricas de tipo integral pueden causar un desbordamiento cuando el resultado se almacena en una variable que es demasiado pequeña para mantenga el valor resultante.
La declaración de espera Si marca un método con el modificador asíncrono, puede usar el operador de espera en el método. Cuando el control alcanza una expresión de espera en el método asíncrono, el control regresa a la persona que llama y el progreso en el método se suspende hasta que se completa la tarea esperada. Cuando se completa la tarea, la ejecución puede reanudarse en el método. Para ver un ejemplo sencillo, consulte la sección "Métodos asíncronos" de Métodos. Para obtener más información, consulte Programación asincrónica con async y await.
La declaración de retorno de rendimiento Un iterador realiza una iteración personalizada sobre una colección, como una lista o una matriz. Un iterador utiliza la declaración de rendimiento y retorno para devolver cada elemento uno a la vez. Cuando se alcanza una declaración de rendimiento, se recuerda la ubicación actual en el código. La ejecución se reinicia desde esa ubicación cuando se llama al iterador la próxima vez.


La declaración fija La declaración fija evita que el recolector de basura reubique una variable móvil. Para obtener más información, consulte fijo.
La declaración de bloqueo La declaración de bloqueo le permite limitar el acceso a bloques de código a un solo subproceso a la vez. Para obtener más información, consulte bloquear.
Declaraciones etiquetadas Puede asignar una etiqueta a una declaración y luego usar la palabra clave goto para saltar a la declaración etiquetada. (Vea el ejemplo en la siguiente fila).
La declaración vacía La declaración vacía consta de un solo punto y coma. No hace nada y se puede utilizar en lugares donde se requiere una declaración pero no es necesario realizar ninguna acción.


 

Declaraciones de declaración

 

El siguiente código muestra ejemplos de declaraciones de variables con y sin asignación inicial, y una declaración constante con la inicialización necesaria.
// Declaraciones de declaración de variables.
double area;
double radius = 2;

// Declaración de declaración constante. const double pi = 3.14159;


 

Declaraciones de expresión

 
El siguiente código muestra ejemplos de declaraciones de expresión, incluida la asignación, la creación de objetos con asignación y la invocación de métodos.

// Declaración de expresión (tarea).
area = 3.14 * (radius * radius);

// Error. No declaración porque no hay asignación: //circ * 2;
// Declaración de expresión (invocación de método). System.Console.WriteLine();
// Declaración de expresión (creación de nuevo objeto). ystem.Collections.Generic.List strings = new System.Collections.Generic.List();


 

La declaración vacía

 

Los siguientes ejemplos muestran dos usos de una declaración vacía:

void ProcessMessages()
{
    while (ProcessMessage())
        ; // Statement needed here.
}

void F() { //... if (done) goto exit; //... exit: ; // Statement needed here. }


 

Declaraciones incrustadas

 

Algunas declaraciones, por ejemplo, las declaraciones de iteración, siempre tienen una declaración incrustada que las sigue. Esta declaración incrustada puede ser una sola declaración o varias declaraciones encerradas entre corchetes {} en un bloque de declaración. Incluso las declaraciones incrustadas de una sola línea se pueden encerrar entre {} corchetes, como se muestra en el siguiente ejemplo:

// Estilo recomendado. Declaración incrustada en bloque.
foreach (string s in System.IO.Directory.GetDirectories(
                        System.Environment.CurrentDirectory))
{
    System.Console.WriteLine(s);
}

// No recomendado. foreach (string s in System.IO.Directory.GetDirectories( System.Environment.CurrentDirectory)) System.Console.WriteLine(s);


Una declaración incrustada que no está entre corchetes {} no puede ser una declaración o una declaración etiquetada. Esto se muestra en el siguiente ejemplo:

si(puntoB == verdadero)
    //Error CS1023:
     int radius = 5;


Coloque la declaración incrustada en un bloque para corregir el error:

si (b == true)
{
    // DE ACUERDO:
    System.DateTime d = System.DateTime.Now;
    System.Console.WriteLine(d.ToLongDateString());
}


 

Bloques de declaraciones anidados

 

Los bloques de instrucciones se pueden anidar, como se muestra en el siguiente código:

foreach (string s in System.IO.Directory.GetDirectories(
    System.Environment.CurrentDirectory))
{
    if (s.StartsWith("CSharp"))
    {
        if (s.EndsWith("TempFolder"))
        {
            return s;
        }
    }
}
return "No encontrado.";



 

Declaraciones inalcanzables

 

Si el compilador determina que el flujo de control nunca puede llegar a una declaración particular bajo ninguna circunstancia, generará la advertencia CS0162, como se muestra en el siguiente ejemplo:

// Un ejemplo demasiado simplificado de código inalcanzable.
const int val = 5;
if (val < 4)
{
    System.Console.WriteLine("Nunca escribiré nada."); //CS0162
}


Atributos


Los atributos proporcionan un método potente para asociar metadatos o información declarativa con código (ensamblados, tipos, métodos, propiedades, etc.). Después de asociar un atributo con una entidad de programa, el atributo se puede consultar en tiempo de ejecución mediante una técnica llamada reflexión.

Los atributos tienen las siguientes propiedades:

Los atributos agregan metadatos a su programa. Los metadatos son información sobre los tipos definidos en un programa. Todos los ensamblados .NET contienen un conjunto específico de metadatos que describen los tipos y miembros de tipo definidos en el ensamblado. Puede agregar atributos personalizados para especificar cualquier información adicional que sea necesaria.
Puede aplicar uno o más atributos a ensamblajes, módulos o elementos de programa más pequeños, como clases y propiedades.
Los atributos pueden aceptar argumentos de la misma manera que los métodos y propiedades.
.Su programa puede examinar sus propios metadatos o los metadatos de otros programas mediante el uso de la reflexión.

Reflection proporciona objetos (de tipo Tipo) que describen ensamblajes, módulos y tipos. Puede utilizar la reflexión para crear dinámicamente una instancia de un tipo, vincular el tipo a un objeto existente u obtener el tipo de un objeto existente e invocar sus métodos o acceder a sus campos y propiedades. Si está utilizando atributos en su código, la reflexión le permite acceder a ellos. Para obtener más información, consulte Atributos.

A continuación se muestra un ejemplo sencillo de reflexión utilizando el método GetType() (heredado por todos los tipos de la clase base Object) para obtener el tipo de una variable:

// Usando GetType para obtener información de tipo:
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);



La salida es: System.Int32.

El siguiente ejemplo utiliza la reflexión para obtener el nombre completo del ensamblado cargado.

// Usando Reflection para obtener información de un Ensamblaje:
Assembly info = typeof(int).Assembly;
Console.WriteLine(info);


El resultado es algo así como: System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e.

 

Usando atributos

 

Los atributos se pueden colocar en casi cualquier declaración, aunque un atributo específico puede restringir los tipos de declaraciones en las que es válido. En C#, se especifica un atributo colocando el nombre del atributo entre corchetes ([]) encima de la declaración de la entidad a la que se aplica.

En este ejemplo, el atributo SerializableAttribute se utiliza para aplicar una característica específica a una clase:

[Serializable]
public class SampleClass
{
    // Objects of this type can be serialized.
}

Un método con el atributo DllImportAttribute se declara como en el siguiente ejemplo:
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();


Se puede colocar más de un atributo en una declaración, como muestra el siguiente ejemplo:
void MethodA([In][Out] ref double x) { }
void MethodB([Out][In] ref double x) { }
void MethodC([In, Out] ref double x) { }

Algunos atributos se pueden especificar más de una vez para una entidad determinada. Un ejemplo de un atributo multiuso es ConditionalAttribute:

[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
    // ...
}

 

Parámetros de atributos

 

Muchos atributos tienen parámetros, que pueden ser posicionales, sin nombre o con nombre. Todos los parámetros posicionales deben especificarse en un orden determinado y no se pueden omitir. Los parámetros con nombre son opcionales y se pueden especificar en cualquier orden. Los parámetros posicionales se especifican primero. Por ejemplo, estos tres atributos son equivalentes:
[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]


El primer parámetro, el nombre de la DLL, es posicional y siempre aparece primero; los demás tienen nombre. En este caso, ambos parámetros con nombre tienen el valor predeterminado falso, por lo que se pueden omitir. Los parámetros posicionales corresponden a los parámetros del constructor de atributos. Los parámetros con nombre u opcionales corresponden a propiedades o campos del atributo. Consulte la documentación del atributo individual para obtener información sobre los valores de parámetros predeterminados.

 

Objetivos de atributos

 

El destino de un atributo es la entidad a la que se aplica el atributo. Por ejemplo, un atributo puede aplicarse a una clase, un método particular o un conjunto completo. Por defecto, un atributo se aplica al elemento que le sigue. Pero también puede identificar explícitamente, por ejemplo, si un atributo se aplica a un método, a su parámetro o a su valor de retorno.

Para identificar explícitamente un destino de atributo, utilice la siguiente sintaxis:
[target : attribute-list]


La lista de posibles valores objetivo se muestra en la siguiente tabla.


Valor objetivoSe aplica a
conjunto Conjunto completo
módulo Módulo de ensamblaje actual
campo Campo en una clase o estructura
evento Evento
método Método u obtener y establecer descriptores de acceso de propiedad
param Parámetros del método o parámetros de acceso de propiedad establecidos
propiedad Propiedad
return Valor de retorno de un método, indexador de propiedades u accesor de propiedad get
tipo Estructura, clase, interfaz, enumeración o delegado
Especificaría el valor objetivo del campo para aplicar un atributo al campo de respaldo creado para una propiedad implementada automáticamente.

El siguiente ejemplo muestra cómo aplicar atributos a ensamblajes y módulos.
using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]


El siguiente ejemplo muestra cómo aplicar atributos a métodos, parámetros de método y valores de retorno de método en C#.

// predeterminado: se aplica al método
[ValidatedContract]
int Method1() { return 0; }

// se aplica al método [method: ValidatedContract] int Method2() { return 0; }
// se aplica al parámetro int Method3([ValidatedContract] string contract) { return 0; }
// se aplica al valor de retorno [return: ValidatedContract] int Method4() { return 0; }


 

Usos comunes de los atributos.

 

La siguiente lista incluye algunos de los usos comunes de los atributos en el código:

.Marcar métodos utilizando el atributo WebMethod en servicios web para indicar que el método debe poder llamarse a través del protocolo SOAP. Para obtener más información, consulte WebMethodAttribute.
.Describir cómo ordenar los parámetros del método cuando se interopera con código nativo. Para obtener más información, consulte MarshalAsAttribute.
.Describir las propiedades COM para clases, métodos e interfaces.
.Llamar a código no administrado mediante la clase DllImportAttribute.
.Describir su ensamblaje en términos de título, versión, descripción o marca registrada.
.Describir qué miembros de una clase serializar para lograr persistencia.
.Describir cómo mapear entre miembros de la clase y nodos XML para la serialización XML.
.Describir los requisitos de seguridad de los métodos.
.Especificar las características utilizadas para hacer cumplir la seguridad.
.Controlar las optimizaciones mediante el compilador justo a tiempo (JIT) para que el código siga siendo fácil de depurar.
.Obtener información sobre el llamador a un método.

 

Resumen de reflexión

 

La reflexión es útil en las siguientes situaciones:

.Cuando tienes que acceder a atributos en los metadatos de tu programa.
.Para examinar y crear instancias de tipos en un ensamblaje.
.Para crear nuevos tipos en tiempo de ejecución. Utilice clases en System.Reflection.Emit.
.Para realizar enlace tardío, acceder a métodos en tipos creados en tiempo de ejecución.
Leave comment
          

Guardar apodo y correo electrónico en este navegador para la próxima vez.



Cargando...     

Dazzle with your smile!