Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=a34eab9d-be8c-488d-b95e-1fcfba55510b

La Evolución del lenguaje C#

Este artículo es una traducción de "The Evolution of C#" originalmente escrito por el MVP de Microsoft, Damir Arh en DotNetCurry. Este artículo fue traducido con el permiso expreso del autor, de DotNetCurry y de la revista DNC, que es una revista gratuita para desarrolladores .NET y JavaScript (en inglés).

Aunque ya han pasado más de 15 años desde el lanzamiento de la primera versión de C#, el lenguaje no parece muy antiguo. Una de las razones es que ha sido actualizado con regularidad.

Cada dos o tres años, se lanzaba una nueva versión con nuevas funcionalidades en el lenguaje. Desde la aparición de C# 7.0 a principios de 2017, la cadencia de actualizaciones ha aumentado todavía más mediante versiones menores del lenguaje.

En el plazo de un año, se publicaron tres nuevas versiones menores (C# 7.1, 7.2 y 7.3).

Lea más en C# 7.1, 7.2 y 7.3 - Nuevas funcionalidades (Actualizado) (en inglés).

Si nos fijásemos en código escrito con C# 1.0 en 2002, se vería muy distinto al código que utilizamos hoy en día.

La mayor parte de las diferencias son el resultado de la utilización de construcciones del lenguaje que no existían en aquel entonces. Sin embargo, junto con el desarrollo del lenguaje, también se añadieron nuevas clases a la plataforma .NET que se benefician de las nuevas capacidades del lenguaje. Todo esto hace que el C# de ahora sea mucho más expresivo y conciso.

Llevemos a cabo una incursión histórica haciendo un repaso de las principales versiones de C#.

Para cada versión, analizaremos los cambios más importantes y compararemos el código que podría ser programado después de su lanzamiento, con el que se tenía que crear antes. Cuando lleguemos a C# 1.0, difícilmente seremos capaces de reconocer el código como C#.

Versiones y Evolución de C#

Imagen ornamental

C# 7.0

En el momento en el que se escribió este artículo, la última versión principal era la 7.0. Su lanzamiento fue en 2017, que sigue siendo bastante reciente, y por lo tanto sus nuevas funcionalidades aún no se utilizan a menudo.

La mayoría de nosotros todavía estamos muy habituados a programar en C# sin aprovechar su potencia ni todas las ventajas que nos ofrece.

La característica principal de C# 7.0 era la búsqueda de patrones que añadía soporte en la comprobación de tipos en sentencias switch:

switch (weapon)
{
    case Sword sword when sword.Durability > 0:
        enemy.Health -= sword.Damage;
        sword.Durability--;
        break;
    case Bow bow when bow.Arrows > 0:
        enemy.Health -= bow.Damage;
        bow.Arrows--;
        break;
}

Hay múltiples nuevas funcionalidades del lenguaje utilizadas en el compacto fragmento de código anterior:

  • Las instrucciones case comprueban el tipo de valor de la variable weapon.
  • En la misma instrucción, declaro una nueva variable del tipo coincidente que se puede utilizar en el bloque de código correspondiente.
  • La última parte de la instrucción después de la palabra clave when especifica una condición adicional para restringir aún más la ejecución del código.

Por otra parte, el operador is se amplió con soporte para hacer también búsqueda de patrones, por lo que ahora se puede utilizar para declarar una nueva variable similar a las instrucciones case:

if (weapon is Sword sword)
{
    // código con la nueva variable de ámbito "sword"
}

En las versiones anteriores del lenguaje sin todas estas funcionalidades, el bloque de código equivalente hubiese sido mucho más largo:

if (weapon is Sword)
{
    var sword = weapon as Sword;
    if (sword.Durability > 0)
    {
        enemy.Health -= sword.Damage;
        sword.Durability--;
    }
}
else if (weapon is Bow)
{
    var bow = weapon as Bow;
    if (bow.Arrows > 0)
    {
        enemy.Health -= bow.Damage;
        bow.Arrows--;
    }
}

En C# 7.0 se añadieron también otras funcionalidades menores:

(i) Las variables out

Que permiten la declaración de variables en el lugar donde se usan por primera vez como argumentos de un método.

if (dictionary.TryGetValue(key, out var value))
{
    return value;
}
else
{
    return null;
}

Antes de que se añadiese esta característica, teníamos que declarar la variable de valor por anticipado:

string value;

if (dictionary.TryGetValue(key, out value))
{
    return value;
}
else
{
    return null;
}

(ii) Tuplas

Se pueden utilizar tuplas para agrupar múltiples variables en un único valor sobre la marcha, según sea necesario, por ejemplo, para los valores de retorno de los métodos:

public (int weight, int count) Stocktake(IEnumerable<IWeapon> weapons)
{
    return (weapons.Sum(weapon => weapon.Weight), weapons.Count());
}

Sin ellas teníamos que declarar un nuevo tipo para hacerlo, incluso si sólo lo necesitábamos en un único sitio:

public Inventory Stocktake(IEnumerable<IWeapon> weapons)
{
    return new Inventory
    {
        Weight = weapons.Sum(weapon => weapon.Weight),
        Count = weapons.Count()
    };
}

Para saber más sobre las nuevas características de C# 7.0, consulta mi artículo C# 7 - ¿Qué hay de nuevo? (en inglés) de una edición anterior de la revista DNC. Para saber más sobre las ediciones menores de C# 7, consulta mi artículo C# 7.1, 7.2 y 7.3 - Novedades (Actualizado) (en inglés) en la revista DNC.

C# 6.0

C# 6.0 fue lanzado en 2015. Coincidió con la completa reprogramación del compilador, denominado Roslyn. Una parte importante de esta versión fueron los servicios de compilación que desde entonces se han utilizado con gran éxito en Visual Studio y otros editores:

  • Visual Studio 2015 y 2017 lo utilizan para el resaltado de sintaxis, navegación por el código, refactorización y otras funciones de edición de código.
  • Muchos otros editores, como Visual Studio Code, Sublime Text, Emacs y otros, proporcionan funcionalidades similares con la ayuda de OmniSharp, un conjunto independiente de herramientas para C# diseñado para integrarse en los editores de código.
  • Muchos analizadores de código estáticos de terceros utilizan los servicios del lenguaje como base. Éstos pueden ser usados dentro de Visual Studio, pero también en el proceso de compilación.

Para obtener más información sobre Roslyn y los servicios de compilador, consulta mi artículo Plataforma de compilador .NET (a.k.a. Roslyn) - Una visión general (en inglés) en la revista DotNetCurry (DNC).

Solamente hubo unos pocos cambios en el lenguaje. Eran en su mayoría azúcar sintáctico, pero muchos de ellos siguen siendo lo suficientemente provechosos como para ser utilizados hoy en día:

Inicializador de diccionarios

Un inicializador de diccionario se puede utilizar para establecer el valor inicial de un diccionario:

var dictionary = new Dictionary<int, string>
{
    [1] = "One",
    [2] = "Two",
    [3] = "Three",
    [4] = "Four",
    [5] = "Five"
};

Sin él, tenías que utilizar en su lugar un inicializador de colecciones:

var dictionary = new Dictionary<int,string>()
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" },
    { 4, "Four" },
    { 5, "Five" }
}

Operador nameof

El operador 'nameof' devuelve el nombre de un símbolo:

public void Method(string input)
{
    if (input == null)
    {
        throw new ArgumentNullException(nameof(input));
    }
    // Implementación del método
}

Es ideal para evitar el uso de cadenas de texto en código que fácilmente pueden desincronizarse cuando se renombran los símbolos:

public void Method(string input)
{
    if (input == null)
    {
        throw new ArgumentNullException("input");
    }
    // Implementación del método
}

Operador de condición de nulo

El operador de condición null reduce el proceso de comprobación de los valores nulos:

var length = input?.Length ?? 0;

No solo se necesita más código para conseguir lo mismo sin él, sino que es mucho más probable que nos olvidemos de añadir esa comprobación:

int length;
if (input == null)
{
    length =0;
}
else
{
    length = input.Length;
}

Importación estática

La importación estática permite la invocación directa de métodos estáticos:

using static System.Math;
var sqrt = Sqrt(input);

Antes de su implantación, era necesario hacer referencia siempre a su clase estática:

var sqrt = Math.Sqrt(input);

La interpolación de cadenas de texto

La interpolación de cadenas simplificó el formateado de las mismas:

var output = $"Length of '{input}' is {input.Length} characters.";

No sólo evita la llamada a String.format, sino que también facilita la lectura del patrón de formato:

var output = String.Format("Length of '{0}' is {1} characters.", input, input.Length);

Por no hablar de que tener argumentos del patrón de formato fuera del patrón hace que sea más probable que se enumeren en el orden equivocado.

Para saber más sobre C# 6, puedes leer mi artículo Actualizando el actual Código C# a C# 6.0 (en inglés) en la revista DotNetCurry (DNC).

C# 5.0

Microsoft lanzó C# 5.0 en 2012 e introdujo una nueva función de lenguaje muy importante: la sintaxis async/await para llamadas asíncronas.

Hizo que la programación asíncrona fuera mucho más accesible para todos. La funcionalidad iba acompañada de un amplio conjunto de nuevos métodos asíncronos para operaciones de entrada y salida en el framework .NET 4.5, que se lanzó al mismo tiempo.

Con la nueva sintaxis, el código asíncrono se parecía mucho al código sincrónico:

public async Task<int> CountWords(string filename)
{
    using (var reader = new StreamReader(filename))
    {
        var text = await reader.ReadToEndAsync();
        return text.Split(' ').Length;
    }
}

En caso de que no estés familiarizado con las palabras clave async y await, ten en cuenta que la llamada de I/O al método ReadToEndAsync no tiene bloqueos. La palabra clave await libera el hilo para otros usos hasta que el fichero leído se complete de forma asíncrona. Es entonces cuando la ejecución continúa de nuevo en el mismo hilo (la mayoría de las veces).

Para saber más sobre async/await, consulta mi artículo Programación asíncrona en C# usando Async Await - Buenas prácticas (en inglés) en la revista DotNetCurry (DNC).

Sin la sintaxis async/await, el mismo código sería mucho más difícil de escribir y de entender:

public Task<int> CountWords(string filename)
{
    var reader = new StreamReader(filename);
    return reader.ReadToEndAsync()
        .ContinueWith(task =>
        {
            reader.Close();
            return task.Result.Split(' ').Length;
        });
}

Nótese cómo debo componer manualmente la continuación de la tarea usando el método Task.ContinueWith.

Tampoco puedo utilizar la instrucción using para cerrar el flujo porque sin la palabra clave await para pausar la ejecución del método, el flujo podía cerrarse antes de que la lectura asíncrona se completase.

E incluso este código está usando el método ReadToEndAsync añadido al framework .NET cuando se liberó C# 5.0. Anteriormente, sólo se disponía de una versión síncrona del método. Para liberar el hilo de llamada durante su duración, tenía que estar envuelto en una tarea, (Task):

public Task<int> CountWords(string filename)
{
    return Task.Run(() =>
    {
        using (var reader = new StreamReader(filename))
        {
            return reader.ReadToEnd().Split(' ').Length;
        }
    });
}

Aunque esto permitió que el hilo de llamada (normalmente el hilo principal o el hilo de interfaz de usuario) realizase otra tarea durante la operación de E/S (Entrada/Salida), otro hilo del conjunto de hilos todavía estaba bloqueado durante ese tiempo. Este código sólo tiene apariencia de asíncrono, pero sigue siendo sincrónico en su núcleo.

Para realizar operaciones de E/S asíncronas reales es necesario utilizar una API mucho más antigua y básica en la clase FileStream:

public Task<int> CountWords(string filename)
{
    var fileInfo = new FileInfo(filename);
 
    var stream = new FileStream(filename, FileMode.Open);
    var buffer = new byte[fileInfo.Length];
    return Task.Factory.FromAsync(stream.BeginRead, stream.EndRead, buffer, 0, buffer.Length, null)
        .ContinueWith(_ =>
        {
            stream.Close();
            return Encoding.UTF8.GetString(buffer).Split(' ').Length;
        });
}

Sólo había un único método asíncrono disponible para la lectura de archivos y sólo nos permitía leer los bytes de un archivo en pequeños trozos, por lo tanto, nosotros mismos estamos decodificando el texto.

Además, el código anterior lee todo el archivo a la vez, lo que no escala bien cuando se trata de archivos grandes. Y seguimos utilizando el método de ayuda FromAsync, que sólo se introdujo en el framework 4 de .NET junto con la propia clase Task. Anteriormente nos teníamos que conformar con usar el patrón del modelo de programación asíncrono (APM) directamente en todas las partes de nuestro código, teniendo que llamar a la pareja de métodos BeginOperation y EndOperation para cada operación asíncrona.

No es de extrañar que las I/Os asíncronas raramente se utilizaran antes de C# 5.0.

C# 4.0

En 2010, se lanzó C# 4.0.

Se enfocó en el enlace dinámico para simplificar la interoperabilidad con COM y los lenguajes dinámicos. Dado que Microsoft Office y muchas otras aplicaciones de gran tamaño pueden ahora ampliarse utilizando la plataforma .NET directamente sin depender de la interoperabilidad COM, vemos poco uso del enlace dinámico en la mayoría del código C# en la actualidad.

Para obtener más información sobre enlaces dinámicos, consulta mi artículo Enlace dinámico en C# (en inglés) en la revista DNC.

No obstante, al mismo tiempo se añadió una funcionalidad importante, que se convirtió en parte esencial del lenguaje y que hoy en día se utiliza con mucha frecuencia sin que se le dé demasiada importancia: los parámetros opcionales y con nombre. Son una gran alternativa a la programación de muchas sobrecargas de la misma función:

public void Write(string text, bool centered = false, bool bold = false)
{
    // output text
}

Este único método puede ser llamado proporcionándole cualquier combinación de parámetros opcionales:

Write("Sample text");
Write("Sample text", true);
Write("Sample text", false, true);
Write("Sample text", bold: true);

Antes de C# 4.0, para lograr este resultado teníamos que escribir tres sobrecargas diferentes:

public void Write(string text, bool centered, bool bold)
{
    // output text
}
 
public void Write(string text, bool centered)
{
    Write(text, centered, false);
}
 
public void Write(string text)
{
    Write(text, false);
}

Y aún así, este código sólo es compatible con las tres primeras llamadas del ejemplo original. Sin los parámetros nombrados, tendríamos que crear un método adicional con un nombre diferente para soportar la última combinación de parámetros, es decir, para especificar sólo los parámetros de texto y negrita, pero manteniendo el valor por defecto para el parámetro centered:

public void WriteBold(string text, bool bold)
{
    Write(text, false, bold);
}

C# 3.0

C# 3.0 del año 2007 fue otro hito importante en el desarrollo del lenguaje. Las funcionalidades introducidas giran en torno a la posibilidad de utilizar LINQ (Language INtegrated Query):

  • Los métodos de extensión aparentan ser llamados como miembros de un tipo aunque estén definidos en otra parte.
  • Las expresiones lambda proporcionan una sintaxis más corta para los métodos anónimos.
  • Los tipos anónimos son tipos ad-hoc que no tienen que ser definidos de antemano.

Todo lo anterior nos ha llevado al método de sintaxis LINQ al que todos estamos tan acostumbrados hoy en día:

var minors = persons.Where(person => person.Age < 18)
.Select(person => new { person.Name, person.Age })
.ToList();

Antes de C# 3.0, no había manera de escribir dicho código declarativo en C#. La funcionalidad tenía que ser codificada imperativamente:

List<NameAndAge> minors = new List<NameAndAge>();
foreach(Person person in persons)
{
    if (person.Age > 18)
    {
        minors.Add(new NameAndAge(person.Name, person.Age));
    }
}

Obsérvese cómo usé el tipo completo para declarar la variable en la primera línea de código. La palabra clave var que la mayoría de nosotros usamos todo el tiempo también fue introducida en C# 3.0. Aunque este código no parece mucho más largo que la versión LINQ, hay que tener en cuenta que todavía tenemos que definir el tipo NameAndAge:

public class NameAndAge
{
    private string name;
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }
 
    private int age;
    public int Age
    {
        get
        {
            return age;
        }
        set
        {
            age = value;
        }
    }
 
    public NameAndAge(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

El código de clase es mucho más largo de lo que estamos acostumbrados debido a otras dos funciones que fueron añadidas en C# 3.0:

  • Sin las propiedades auto-implementadas debo declarar manualmente los campos de respaldo, así como los getters y setters triviales para cada propiedad.
  • El constructor para establecer los valores de las propiedades es necesario porque no había sintaxis del inicializador antes de C# 3.0.

C# 2.0

Hemos llegado al año 2005 cuando se lanzó C# 2.0. Muchos consideran que esta es la primera versión del lenguaje lo suficientemente madura para ser utilizada en proyectos reales. Introdujo muchas funcionalidades de las que no podemos prescindir hoy en día, aunque la más importante e impactante de todas fue, sin duda, el soporte de los genéricos.

Nadie se puede imaginar C# sin genéricos. Todas las colecciones que seguimos utilizando hoy en día son genéricas:

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
 
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += numbers[i];
}

Sin genéricos, no había colecciones fuertemente tipadas en el framework .NET. En lugar del código de arriba, nos teníamos que conformar con lo siguiente:

ArrayList numbers = new ArrayList();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
 
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += (int)numbers[i];
}

Aunque el código puede parecer similar, hay una diferencia importante: este código no es de tipado seguro. Fácilmente podría añadir valores de otros tipos a la colección, no sólo int. Además, fíjate cómo convierto el valor que obtengo de la colección antes de usarla. Debo hacerlo porque está tipado como objeto dentro de la colección.

Por supuesto, este código es muy propenso a contener errores. Afortunadamente, había otra solución disponible si quería tener tipado seguro. Podría crear mi propia colección tipada:

public class IntList : CollectionBase
{
    public int this[int index]
    {
        get
        {
            return (int)List[index];
        }
        set
        {
            List[index] = value;
        }
    }
 
    public int Add(int value)
    {
        return List.Add(value);
    }
 
    public int IndexOf(int value)
    {
        return List.IndexOf(value);
    }
 
    public void Insert(int index, int value)
    {
        List.Insert(index, value);
    }
 
    public void Remove(int value)
    {
        List.Remove(value);
    }
 
    public bool Contains(int value)
    {
        return List.Contains(value);
    }
 
    protected override void OnValidate(Object value)
    {
        if (value.GetType() != typeof(System.Int32))
        {
            throw new ArgumentException("Value must be of type Int32.", "value");
        }
    }
}

El código que lo usa seguiría siendo similar, pero al menos sería de tipado seguro, tal y como estamos acostumbrados con los genéricos:

IntList numbers = new IntList();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
 
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += numbers[i];
}

Sin embargo, la colección IntList sólo se puede utilizar para almacenar int. Debo implementar una colección de tipado fuerte diferente si quiero almacenar valores de un tipo diferente, repitiendo código de manera absurda.

Y el código resultante sigue siendo significativamente menos eficaz para los tipos de valor porque se debe hacer un boxing de los valores antes de ser almacenados en la colección y se debe hacer un unboxing cuando se necesitan.

Hay muchas otras funcionalidades sin las que no podemos vivir hoy en día y que no fueron implementadas antes de C# 2.0:

  • Tipos anulables
  • Iteradores
  • Métodos anónimos
  • ...

Conclusión

C# ha sido el lenguaje principal para el desarrollo en .NET desde la versión 1.0, pero ha evolucionado mucho desde entonces. Gracias a las funcionalidades que se le fueron añadiendo versión tras versión, se mantiene al día con las nuevas tendencias del mundo de la programación y sigue siendo una buena alternativa a los nuevos lenguajes que han aparecido desde entonces.

Ocasionalmente, incluso logra iniciar una nueva tendencia como lo hizo con las palabras clave async y await, que más tarde han sido adoptadas por otros lenguajes. Con soporte para tipos de referencia anulables y muchas otras nuevas funciones anunciadas para C# 8.0, no hay que temer que el lenguaje deje de evolucionar.

Si estás interesado en conocer las próximas características de C# 8, consulta este post (en inglés).

Este artículo ha sido revisado técnicament por Yacoub Massad.

campusMVP campusMVP es la mejor forma de aprender a programar online y en español. En nuestros cursos solamente encontrarás contenidos propios de alta calidad (teoría+vídeos+prácticas) creados y tutelados por los principales expertos del sector. Nosotros vamos mucho más allá de una simple colección de vídeos colgados en Internet porque nuestro principal objetivo es que tú aprendas. Ver todos los posts de campusMVP
Archivado en: Lenguajes y plataformas

¿Te ha gustado este post?
Pues espera a ver nuestra newsletter...

Suscríbete a la newsletter

La mejor formación online para desarrolladores como tú

Comentarios (2) -

Tengo una consulta, las versiones de c# depende del framework .net?. Por ejemplo si tengo  el framework 4.5 solo puedo trabjar con cierta versión de C#.

Es que como se tiene varias versiones, como podemos saber con que versión  de C# estamos trabajando, y si queremos trabajar con alguna versión de c# que debemos hacer.

Muy buen articulo, Gracias.

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola Jherson:

La respuesta a tu pregunta es no y sí al mismo tiempo (soy gallego, no puedo evitar esta indefinición 😉).

Me explico mejor...

Una cosa es el lenguaje y otra es la plataforma y son cosas independientes. Por lo tanto puedes elegir una versión concreta del lenguaje (del compilador, al final) y usar una versión cualquiera de la plataforma.

Lo que ocurre es que, hasta C# 5, el compilador del lenguaje se distribuía con .NET y con el SDK correspondiente. Es decir, que hasta C# 5 la versión del framework te condicionaba la versión del lenguaje. De este modo, por ejemplo, si en 2015 querías usar algo de C# 5 como por ejemplo async/await) tenías que utilizar .NET 4.5, ya que el compilador de C#, csc.exe, venía con esa versión del Framework.

Cómo comenta Damir en el artículo, a partir de 2016 y C# 6 entró en escena Roslyn el compilador de nueva generación de Microsoft que ya se distribuía de manera independiente a la plataforma, de modo que el lenguaje y .NET pudieran evolucionar por separado. A partir de entonces es posible fácilmente usar una versión diferente del lenguaje de la que se supone que se viene con cada versión del framework.

En Visual Studio puedes jugar con estos dos parámetros de manera muy simple. Si vas a las propiedades de un proyecto, dentro de la pestaña "Application" puedes elegir la versión del framework que quieres utilizar, y en la pestaña "Build", dentro de "Advanced" puedes elegir la versión del lenguaje. Te lo muestro en este pequeño vídeo:

https://cl.ly/ce0ecfa79996

En él puedes ver cómo cambiar ambas cosas: la versión del framework y la del lenguaje C#.

Entonces, en resumen: hasta C# 5 ambas cosas iban unidas, pero a partir de C# 6 y Roslyn ya no es así y en la actualidad puedes elegirlas de manera independiente.

Espero que esto te lo aclare.

Saludos!

Responder

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.