domingo, 30 de noviembre de 2025

Programando en C# no. 13: tipo Nullable

En C#, un tipo nullable (Nullable <T> o T?) permite que un tipo de valor (como int, bool, double) pueda contener además el valor null. El valor null se utiliza para representar una ausencia de valor. Sin embargo, los tipos de valor (como int, double, bool) no pueden ser null por defecto, siempre tienen un valor predeterminado.

Esto es útil cuando un valor puede ser opcional o no siempre está disponible.

Ejemplo: crear variables tipo int y bool con valor null.

int? edad = null;     // válido
bool? activo = true;  // válido

Aquí usamos la sintaxis corta T? para incluir null. La sintaxis larga sería como esto:

Nullable<int> numero1 = null;
Nullable<string> cadena = null;             

Un ejemplo más práctico sería como esto:

int? edad = null;

if (edad.HasValue) {
    Console.WriteLine("Edad: " + edad.Value);
} else {
    Console.WriteLine("Edad no especificada");
}         

Operadores útiles:

  • ?? → operador de coalescencia nula (valor por defecto si es null). 
  •  ?. → acceso seguro a miembros (C# 6+).

Ejemplo de uso:

int? valor = null;
int resultado = valor ?? -1; // devuelve -1 si valor es null    

Usando el método GetValueOrDefault() :

int? numero = null;
Console.WriteLine(numero.GetValueOrDefault()); // imprime 0  

Para los programadores Java esto se les hará familiar, pues existe una clase genérica llamada Optional que es similar a Nullable<T>. Miremos un ejemplo comparativo:

En Java:

Optional<String> usuario = Optional.ofNullable(null);
System.out.println(usuario.orElse("Invitado"));

En C#:

string? usuario = null;
Console.WriteLine(usuario ?? "Invitado");

Métodos principales de Nullable<T>

He aquí una tabla de los principales métodos:

Método/Propiedad Propósito Ejemplo Nota clave
HasValue Indica si contiene un valor if (x.HasValue) { ... } true si no es null
Value Devuelve el valor almacenado int v = x.Value; Lanza InvalidOperationException si es null
GetValueOrDefault() Devuelve el valor o el default del tipo int v = x.GetValueOrDefault(); Si es null, devuelve 0 para int, false para bool, etc.
GetValueOrDefault(defaultValue) Devuelve el valor o un valor por defecto especificado int v = x.GetValueOrDefault(-1); Útil para valores personalizados
ToString() Convierte el valor a cadena Console.WriteLine(x.ToString()); Devuelve "" si es null

Ahora comparemos la clase Optional de Java con Nullable<T> de C#.

Aspecto Java Optional<T> C# Nullable<T>
Propósito Evitar null en retornos y expresar ausencia de valor de forma explícita Permitir que tipos de valor (int, bool, etc.) puedan ser null
Sintaxis Optional<String> nombre = Optional.of("Carraro"); int? edad = null; (azúcar sintáctico de Nullable<int>)
Tipos soportados Cualquier tipo (referencia o valor) Solo tipos de valor (structs, int, double, bool, etc.)
Creación Optional.of(value) / Optional.ofNullable(value) / Optional.empty() new Nullable<int>(5) / int? x = null;
Acceso al valor get() (lanza excepción si vacío) .Value (lanza excepción si es null)
Verificación de presencia isPresent() / isEmpty() .HasValue
Valores por defecto orElse(default) / orElseGet(Supplier) .GetValueOrDefault() / .GetValueOrDefault(default)
Transformaciones map(Function) / flatMap(Function) / filter(Predicate) No tiene transformaciones funcionales integradas
Acciones condicionales ifPresent(Consumer) / ifPresentOrElse(...) Uso de HasValue + condicionales tradicionales
Excepciones orElseThrow() / orElseThrow(Supplier) .Value lanza InvalidOperationException si es null
Uso recomendado Como tipo de retorno en APIs y Streams para evitar null Para representar datos opcionales en BD, formularios, cálculos
Introducción Java 8 (2014) .NET 2.0 (2005)

Ejemplo. Crear un programa en C# que emplee los métodos Nullable en su sintaxis corta y larga

Program.cs

using System;

class Program
{
    static void Main()
    {
        // 1. empty()
        string? emptyOpt = null;
        Console.WriteLine("emptyOpt: " + (emptyOpt ?? "null"));

        // 2. of()
        string? opt = "Hello";
        Console.WriteLine("opt: " + opt);

        // 3. ofNullable()
        string? nullableOpt = null;
        Console.WriteLine("nullableOpt: " + (nullableOpt ?? "null"));

        // 4. isPresent()
        Console.WriteLine("opt is present: " + (opt != null));
        Console.WriteLine("emptyOpt is present: " + (emptyOpt != null));

        // 5. ifPresent()
        if (opt != null)
        {
            Console.WriteLine("Value in opt: " + opt);
        }

        // 6. isEmpty()
        Console.WriteLine("emptyOpt is empty: " + (emptyOpt == null));

        // 7. get()
        try
        {
            Console.WriteLine("Value from opt: " + opt);
            Console.WriteLine("Value from emptyOpt: " + emptyOpt!); // Forzamos acceso
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from emptyOpt.get(): " + e);
        }

        // 8. orElse()
        string value = opt ?? "Default Value";
        Console.WriteLine("orElse value: " + value);
        string defaultValue = emptyOpt ?? "Default Value";
        Console.WriteLine("orElse default value: " + defaultValue);

        // 9. orElseGet()
        string valueWithSupplier = opt ?? GetDefault();
        Console.WriteLine("orElseGet value: " + valueWithSupplier);
        string defaultValueWithSupplier = emptyOpt ?? GetDefault();
        Console.WriteLine("orElseGet default value: " + defaultValueWithSupplier);

        // 10. orElseThrow()
        try
        {
            string throwValue = emptyOpt ?? throw new ArgumentException("Value not present");
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from orElseThrow: " + e);
        }

        // 11. map()
        int? lengthOpt = opt?.Length;
        Console.WriteLine("Mapped lengthOpt: " + (lengthOpt.HasValue ? lengthOpt.Value.ToString() : "null"));

        // 12. flatMap()
        string? flatMapped = opt != null ? opt.ToUpper() : null;
        Console.WriteLine("FlatMapped opt: " + (flatMapped ?? "null"));

        // 13. filter()
        string? filtered = (opt != null && opt.Length > 3) ? opt : null;
        Console.WriteLine("Filtered opt: " + (filtered ?? "null"));
        string? emptyFiltered = (opt != null && opt.Length > 10) ? opt : null;
        Console.WriteLine("Empty Filtered opt: " + (emptyFiltered ?? "null"));

        // 14. stream() (Java 9+)
        // En C# usamos LINQ o simplemente comprobamos null
        if (opt != null)
        {
            Console.WriteLine(opt);
        }

        // 15. or() (Java 9+)
        string? anotherOpt = opt ?? "Another Value";
        Console.WriteLine("Or opt: " + anotherOpt);
        string? anotherEmptyOpt = emptyOpt ?? "Another Value";
        Console.WriteLine("Or emptyOpt: " + anotherEmptyOpt);

        // 16. equals()
        Console.WriteLine("opt equals anotherOpt: " + (opt == anotherOpt));
        Console.WriteLine("emptyOpt equals anotherEmptyOpt: " + (emptyOpt == anotherEmptyOpt));

        // 17. hashCode()
        Console.WriteLine("opt hashCode: " + (opt?.GetHashCode() ?? 0));
        Console.WriteLine("emptyOpt hashCode: " + (emptyOpt?.GetHashCode() ?? 0));

        // 18. toString()
        Console.WriteLine("opt toString: " + (opt ?? "null"));
        Console.WriteLine("emptyOpt toString: " + (emptyOpt ?? "null"));
    }

    static string GetDefault()
    {
        return "Default Value from Supplier";
    }
}

Ahora con su sintaxis larga:

Program.cs

using System;

class Program
{
    static void Main()
    {
        // 1. empty()
        Nullable<string> emptyOpt = null;
        Console.WriteLine("emptyOpt: " + (emptyOpt ?? "null"));

        // 2. of()
        Nullable<string> opt = "Hello";
        Console.WriteLine("opt: " + opt);

        // 3. ofNullable()
        Nullable<string> nullableOpt = null;
        Console.WriteLine("nullableOpt: " + (nullableOpt ?? "null"));

        // 4. isPresent()
        Console.WriteLine("opt is present: " + opt.HasValue);
        Console.WriteLine("emptyOpt is present: " + emptyOpt.HasValue);

        // 5. ifPresent()
        if (opt.HasValue)
        {
            Console.WriteLine("Value in opt: " + opt.Value);
        }

        // 6. isEmpty()
        Console.WriteLine("emptyOpt is empty: " + !emptyOpt.HasValue);

        // 7. get()
        try
        {
            Console.WriteLine("Value from opt: " + opt.Value);
            Console.WriteLine("Value from emptyOpt: " + emptyOpt.Value); // Lanza excepción
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from emptyOpt.get(): " + e);
        }

        // 8. orElse()
        string value = opt.HasValue ? opt.Value : "Default Value";
        Console.WriteLine("orElse value: " + value);
        string defaultValue = emptyOpt.HasValue ? emptyOpt.Value : "Default Value";
        Console.WriteLine("orElse default value: " + defaultValue);

        // 9. orElseGet()
        string valueWithSupplier = opt.HasValue ? opt.Value : GetDefault();
        Console.WriteLine("orElseGet value: " + valueWithSupplier);
        string defaultValueWithSupplier = emptyOpt.HasValue ? emptyOpt.Value : GetDefault();
        Console.WriteLine("orElseGet default value: " + defaultValueWithSupplier);

        // 10. orElseThrow()
        try
        {
            string throwValue = emptyOpt.HasValue ? emptyOpt.Value : throw new ArgumentException("Value not present");
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from orElseThrow: " + e);
        }

        // 11. map()
        Nullable<int> lengthOpt = opt.HasValue ? opt.Value.Length : (int?)null;
        Console.WriteLine("Mapped lengthOpt: " + (lengthOpt.HasValue ? lengthOpt.Value.ToString() : "null"));

        // 12. flatMap()
        Nullable<string> flatMapped = opt.HasValue ? opt.Value.ToUpper() : null;
        Console.WriteLine("FlatMapped opt: " + (flatMapped.HasValue ? flatMapped.Value : "null"));

        // 13. filter()
        Nullable<string> filtered = (opt.HasValue && opt.Value.Length > 3) ? opt.Value : null;
        Console.WriteLine("Filtered opt: " + (filtered.HasValue ? filtered.Value : "null"));
        Nullable<string> emptyFiltered = (opt.HasValue && opt.Value.Length > 10) ? opt.Value : null;
        Console.WriteLine("Empty Filtered opt: " + (emptyFiltered.HasValue ? emptyFiltered.Value : "null"));

        // 14. stream() (Java 9+)
        if (opt.HasValue)
        {
            Console.WriteLine(opt.Value);
        }

        // 15. or()
        Nullable<string> anotherOpt = opt.HasValue ? opt.Value : "Another Value";
        Console.WriteLine("Or opt: " + anotherOpt);
        Nullable<string> anotherEmptyOpt = emptyOpt.HasValue ? emptyOpt.Value : "Another Value";
        Console.WriteLine("Or emptyOpt: " + anotherEmptyOpt);

        // 16. equals()
        Console.WriteLine("opt equals anotherOpt: " + (opt.Equals(anotherOpt)));
        Console.WriteLine("emptyOpt equals anotherEmptyOpt: " + (emptyOpt.Equals(anotherEmptyOpt)));

        // 17. hashCode()
        Console.WriteLine("opt hashCode: " + (opt.HasValue ? opt.Value.GetHashCode() : 0));
        Console.WriteLine("emptyOpt hashCode: " + (emptyOpt.HasValue ? emptyOpt.Value.GetHashCode() : 0));

        // 18. toString()
        Console.WriteLine("opt toString: " + (opt.HasValue ? opt.Value : "null"));
        Console.WriteLine("emptyOpt toString: " + (emptyOpt.HasValue ? emptyOpt.Value : "null"));
    }

    static string GetDefault()
    {
        return "Default Value from Supplier";
    }
}

Concluyendo:

La utilidad de Nullable <T> es representar datos opcionales de manera segura y expresiva, evitando ambigüedades y errores comunes como el uso de valores ficticios o NullReferenceException. Es especialmente valioso en aplicaciones que interactúan con bases de datos, formularios y cálculos donde ciertos valores pueden estar ausentes.

Enlaces:

https://www.luisllamas.es/csharp-tipos-nullables/
https://comunidadcityjavamex.blogspot.com/2025/11/java-tip-24-conociendo-la-clase-optional.html

No hay comentarios:

Publicar un comentario

Dapper: un micro ORM para .NET

En una anterior entrega vimos lo que era EntityFramework (EF), un ORM para .NET ( C# ). En está ocasión veremos una alternativa: Dap...

Etiquetas

Archivo del blog