Práce s odkazovými typy s možnou hodnotou Null

C# 8 zavedl novou funkci s názvem odkazovatelné odkazové typy s možnou hodnotou null( NRT), která umožňuje, aby odkazové typy byly opatřeny poznámkami, které označují, zda jsou platné, aby obsahovaly nebo null ne. Pokud s touto funkcí začínáte, doporučujeme, abyste se s ní seznámili čtením dokumentace jazyka C#. Odkazové typy s možnou hodnotou null jsou ve výchozím nastavení povoleny v nových šablonách projektů, ale zůstávají v existujících projektech zakázány, pokud se explicitně nezavolí.

Tato stránka představuje podporu typů odkazů s možnou hodnotou null a popisuje osvědčené postupy pro práci s nimi.

Povinné a volitelné vlastnosti

Hlavní dokumentace k požadovaným a volitelným vlastnostem a jejich interakci s odkazovými typy s možnou hodnotou null je stránka Povinné a Volitelné vlastnosti . Doporučujeme začít tím, že si nejprve přečtete tuto stránku.

Poznámka

Při povolování odkazových typů s možnou hodnotou null u existujícího projektu buďte opatrní: vlastnosti typu odkazu, které byly dříve nakonfigurovány jako volitelné, budou nyní nakonfigurovány podle potřeby, pokud nejsou explicitně opatřeny poznámkami, aby byly nullable. Při správě schématu relační databáze může dojít k vygenerování migrací, které změní hodnotu null sloupce databáze.

Nenulové vlastnosti a inicializace

Pokud jsou povoleny odkazové typy s možnou hodnotou null, kompilátor jazyka C# generuje upozornění pro jakoukoli neinicializovanou neinicializovanou vlastnost, která by obsahovala null. V důsledku toho nelze použít následující běžný způsob psaní typů entit:

public class Customer
{
    public int Id { get; set; }

    // Generates CS8618, uninitialized non-nullable property:
    public string Name { get; set; }
}

Pokud používáte C# 11 nebo novější, požadované členy poskytují dokonalé řešení tohoto problému:

public required string Name { get; set; }

Kompilátor teď zaručuje, že když kód vytvoří instanci zákazníka, vždy inicializuje jeho vlastnost Name. A vzhledem k tomu, že sloupec databáze namapovaný na vlastnost není null, všechny instance načtené systémem EF vždy obsahují název, který není null.

Pokud používáte starší verzi jazyka C#, je vazbu konstruktoru alternativní technikou, která zajistí inicializaci nenulových vlastností:

public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

V některých scénářích bohužel není možné použít vazbu konstruktoru; například nelze tímto způsobem inicializovat vlastnosti navigace. V těchto případech můžete jednoduše inicializovat vlastnost null pomocí operátoru null-forgiving (další podrobnosti najdete níže):

public Product Product { get; set; } = null!;

Požadované navigační vlastnosti

Požadované navigační vlastnosti představují další potíže: i když závislý objekt vždy existuje pro daný objekt zabezpečení, může nebo nemusí být načten konkrétním dotazem v závislosti na potřebách v daném okamžiku v programu (viz různé vzory načítání dat). Současně může být nežádoucí, aby tyto vlastnosti null, protože by to vynutilo veškerý přístup k nim kontrolovat null, i když je navigace známo, že je načtena, a proto nemůže být null.

To nemusí být nutně problém! Pokud je požadovaný závislý správně načten (např. prostřednictvím Include), je zaručeno, že přístup k navigační vlastnosti vždy vrátí hodnotu non-null. Na druhou stranu se aplikace může rozhodnout zkontrolovat, zda je relace načtena kontrolou, zda je nullnavigace . V takových případech je vhodné nastavit, aby navigace byla null. To znamená, že požadované navigace ze závislého na objektu zabezpečení:

  • Pokud se považuje za chybu programátora pro přístup k navigaci, pokud není načtena, měla by být nenulová.
  • Pokud je pro kód aplikace přijatelný, měl by mít hodnotu null, aby se zjistilo, jestli je relace načtena, nebo ne.

Pokud chcete přísnější přístup, můžete mít vlastnost, která není nullable s polem s možnou hodnotou null:

private Address? _shippingAddress;

public Address ShippingAddress
{
    set => _shippingAddress = value;
    get => _shippingAddress
           ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}

Pokud je navigace správně načtena, bude závislá přístupná prostřednictvím vlastnosti. Pokud je však vlastnost přístupná bez toho, aby se nejprve správně načítala související entita, vyvolá se chyba InvalidOperationException , protože kontrakt rozhraní API byl použit nesprávně.

Poznámka

Navigace v kolekcích, které obsahují odkazy na více souvisejících entit, by měly být vždy nenulovatelné. Prázdná kolekce znamená, že neexistují žádné související entity, ale samotný seznam by nikdy neměl být null.

DbContext a DbSet

U ef je běžné mít neinicializované vlastnosti DbSet u typů kontextu:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set;}
}

I když to obvykle způsobuje upozornění kompilátoru, EF Core 7.0 a vyšší potlačí toto upozornění, protože EF tyto vlastnosti automaticky inicializuje prostřednictvím reflexe.

Ve starší verzi EF Core můžete tento problém obejít následujícím způsobem:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
}

Další strategií je použít nenulové automatické vlastnosti, ale inicializovat je na null, pomocí operátoru null-forgiving (!) umlčet upozornění kompilátoru. Základní konstruktor DbContext zajišťuje, že všechny vlastnosti DbSet budou inicializovány a hodnota null nebude nikdy pozorována na nich.

Při práci s volitelnými relacemi je možné narazit na upozornění kompilátoru, kde null by skutečná výjimka odkazu nebylo možné. Při překladu a spouštění dotazů LINQ EF Core zaručuje, že pokud neexistuje volitelná související entita, bude se žádná navigace na ni jednoduše ignorovat, a ne vyvolání. Kompilátor však o této zárukě EF Core neví a vygeneruje upozornění, jako by byl dotaz LINQ spuštěný v paměti, s LINQ to Objects. V důsledku toho je nutné pomocí operátoru null-forgiving (!) informovat kompilátor, že skutečná null hodnota není možná:

var order = context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToList();

K podobnému problému dochází v případě, že mezi volitelnými navigacemi zahrnete více úrovní relací:

var order = context.Orders
    .Include(o => o.OptionalInfo!)
    .ThenInclude(op => op.ExtraAdditionalInfo)
    .Single();

Pokud zjistíte, že se to dělá hodně a typy entit, které se v dotazech EF Core používají převážně (nebo výhradně), zvažte, že navigační vlastnosti nejsou nullable a nakonfigurujete je jako volitelné prostřednictvím rozhraní Fluent API nebo datových poznámek. Tím se odeberou všechna upozornění kompilátoru při zachování vztahu nepovinný; Pokud se ale vaše entity procházejí mimo EF Core, můžete pozorovat null hodnoty, i když jsou vlastnosti označené jako nenulové.

Omezení ve starších verzích

Před EF Core 6.0 se použila následující omezení:

  • Veřejná plocha rozhraní API nebyla opatřena poznámkami pro nulovou hodnotu (veřejné rozhraní API bylo "bezpochybné"), což někdy zneužito, když je funkce NRT zapnutá. To zejména zahrnuje asynchronní operátory LINQ zveřejněné EF Core, jako je FirstOrDefaultAsync. Veřejné rozhraní API je plně opatřeno poznámkami pro nulovou hodnotu počínaje EF Core 6.0.
  • Zpětná analýza nepodporuje referenční typy c# 8 s možnou hodnotou null( NRT): EF Core vždy vygeneroval kód C#, který předpokládal, že funkce je vypnutá. Například z textových sloupců s možnou hodnotou null se vygenerovala vlastnost typu string, nikoli string?. Pokud chcete tuto vlastnost nakonfigurovat jako povinnou, použijte Fluent API nebo datové poznámky. Pokud používáte starší verzi EF Core, můžete vygenerovaný kód upravit a nahradit tyto typy poznámkou o hodnotě null v jazyce C#.