FirstOrDefault / SingleOrDefault în C# (.NET)

LINQ (Language Integrated Query) .NET 3.5+ (parametrul defaultValue în .NET 6+)

Extrage un singur element dintr-o secvență (primul găsit sau unicul existent) ori returnează o valoare implicită când nu există potriviri.

Sintaxă C#
var elem = colectie.FirstOrDefault(x => x.Id == id);

Ce face FirstOrDefault / SingleOrDefault

În LINQ există patru metode principale pentru extragerea unui element individual dintr-o secvență: First, FirstOrDefault, Single și SingleOrDefault. Înțelegerea diferenței dintre ele este crucială pentru a evita excepțiile neașteptate în producție. Toate patru sunt metode de execuție imediată (terminală): parcurg secvența pe loc și returnează un singur obiect.

Metodele First și FirstOrDefault se opresc imediat ce găsesc primul element care respectă condiția (fără a mai verifica restul colecției); dacă niciun element nu se potrivește, First aruncă InvalidOperationException, în timp ce FirstOrDefault returnează default(T) (null pentru clase/înregistrări, 0 pentru numere) sau valoarea implicită pasată ca parametru în .NET 6+. În schimb, Single și SingleOrDefault impun o regulă mai strictă: trebuie să existe cel mult un singur element care respectă condiția — dacă găsesc două sau mai multe potriviri, ambele aruncă InvalidOperationException: Sequence contains more than one matching element.

Parametri, metode și opțiuni

Parametru / Metodă Descriere
FirstOrDefault(predicat) Returnează primul element care respectă predicatul sau default(T) (null / 0) dacă nu se găsește nimic.
FirstOrDefault(predicat, defaultValue) Returnează primul element potrivit sau defaultValue dacă secvența nu conține nicio potrivire (.NET 6+).
First(predicat) Returnează primul element potrivit; aruncă InvalidOperationException dacă nu există niciunul.
SingleOrDefault(predicat) Returnează unicul element potrivit, default(T) dacă nu există niciunul, sau aruncă excepție dacă există două sau mai multe potriviri.
Single(predicat) Returnează unicul element potrivit; aruncă excepție atât când sunt 0 potriviri, cât și când sunt două sau mai multe potriviri.
LastOrDefault(predicat) Returnează ultimul element din secvență care respectă condiția sau default(T).

Exemple

Exemplul 1: Căutare sigură cu FirstOrDefault și valoare implicită personalizată (.NET 6+)

Căutăm un utilizator după ID și oferim fie verificarea cu is not null, fie un obiect fallback direct ca al doilea parametru al metodei FirstOrDefault.

var utilizatori = new List<Utilizator>
{
    new(1, "andrei@dexblog.ro", "Admin"),
    new(2, "elena@dexblog.ro", "Editor"),
    new(3, "mihai@dexblog.ro", "Editor")
};

Utilizator? gasit = utilizatori.FirstOrDefault(u => u.Id == 2);
Console.WriteLine($"Căutare ID 2: {gasit?.Email ?? "Inexistent"}");

// Cu valoare implicită explicită (.NET 6+):
Utilizator vizitator = utilizatori.FirstOrDefault(
    u => u.Id == 99,
    defaultValue: new Utilizator(0, "guest@dexblog.ro", "Guest")
);
Console.WriteLine($"Căutare ID 99: {vizitator.Email} (rol: {vizitator.Rol})");

record Utilizator(int Id, string Email, string Rol);

Ieșire în consolă

Căutare ID 2: elena@dexblog.ro
Căutare ID 99: guest@dexblog.ro (rol: Guest)

Exemplul 2: Diferența critică dintre FirstOrDefault și SingleOrDefault la duplicate

SingleOrDefault validează unicitatea: este ideal când cauți după o cheie unică (CNP, e-mail, SKU) și vrei să detectezi imediat o eventuală corupere a datelor.

string[] coduri = ["RO-01", "RO-02", "RO-01"];

string? primul = coduri.FirstOrDefault(c => c == "RO-01");
Console.WriteLine($"FirstOrDefault a returnat: {primul}");

try
{
    string? unicul = coduri.SingleOrDefault(c => c == "RO-01");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"SingleOrDefault a aruncat excepție: {ex.Message}");
}

Ieșire în consolă

FirstOrDefault a returnat: RO-01
SingleOrDefault a aruncat excepție: Sequence contains more than one matching element

Greșeli frecvente

  • Folosirea lui FirstOrDefault pe o colecție de tipuri valoare (precum List<int>) unde 0 este o valoare validă: dacă FirstOrDefault returnează 0, nu poți ști dacă a găsit numărul 0 în listă sau dacă nu a găsit nimic. În acest caz proiectează la nullable (lista.Cast<int?>().FirstOrDefault(...)) sau specifică o valoare santinelă precum FirstOrDefault(x => ..., -1).
  • Folosirea lui SingleOrDefault pe colecții mari în memorie când nu ai nevoie să validezi absența duplicatelor — spre deosebire de FirstOrDefault (care se oprește la primul element găsit), SingleOrDefault este obligat să scaneze restul listei până la capăt pentru a se asigura că nu mai există un al doilea element potrivit.

Funcții și concepte înrudite

  • Enumerable.Where: Filtrează declarativ o secvență de elemente pe baza unui predicat lambda, cu execuție amânată (deferred execution).
  • Tipuri Nullable (?, ??, ?.): Reprezintă explicit absența unei valori și previne excepțiile NullReferenceException prin operatori siguri de navigare și coalescență.
  • Any, All, Count și Sum: Evaluează condiții booleene rapide (cu scurtcircuitare) și calculează agregări numerice (Count, Sum, Average, Min, Max) peste colecții.