Allmänna typer och metoder

Tips/Råd

Är du nybörjare på att utveckla programvara? Börja med självstudierna Komma igång först. Du kommer att stöta på generiska läkemedel så snart du använder samlingar som List<T>.

Har du erfarenhet av ett annat språk? C#-generiska objekt liknar generiska objekt i Java eller mallar i C++, men med fullständig körningstypinformation och ingen typavsplåning. Skumma samlingsuttrycken och samvarians- och kontravariansavsnitten för C#-specifika mönster.

Med generiska objekt kan du skriva kod som fungerar med valfri typ samtidigt som fullständig typ av säkerhet bibehålls. I stället för att skriva separata klasser eller metoder för int, string och alla andra typer du behöver, skriver du en version med en eller flera typparametrar (till exempel T, eller TKey och TValue) och anger de faktiska typerna när du använder den. Kompilatorn kontrollerar typkontroll vid kompileringstillfället, så du behöver inte runtime-typomvandlingar eller risk InvalidCastException.

Du stöter ständigt på generics i C#. Samlingar, asynkrona returtyper, delegater och LINQ förlitar sig alla på generiska typer.

List<int> scores = [95, 87, 72, 91];
Dictionary<string, decimal> prices = new()
{
    ["Widget"] = 19.99m,
    ["Gadget"] = 29.99m
};
Task<string> greeting = Task.FromResult("Hello, generics!");
Func<int, bool> isPositive = n => n > 0;

Console.WriteLine($"First score: {scores[0]}");
Console.WriteLine($"Widget price: {prices["Widget"]:C}");
Console.WriteLine($"Greeting: {await greeting}");
Console.WriteLine($"Is 5 positive? {isPositive(5)}");

I varje fall anger typargumentet i vinkelparenteser (<int>, <string>, <Product>) den generiska typen vilken typ av data det innehåller eller fungerar på. Kompilatorn tillämpar typsäkerhet. Du kan inte av misstag lägga till en string i en List<int>.

Konsumera generiska typer

Ofta använder du generiska typer från .NET-klassbiblioteket i stället för att skapa egna. I följande avsnitt visas de vanligaste generiska typerna som du använder.

Allmänna samlingar

Namnområdet System.Collections.Generic innehåller typsäkra samlingsklasser. Använd alltid dessa samlingar i stället för icke-generiska samlingar som ArrayList:

// A strongly typed list of strings
List<string> names = ["Alice", "Bob", "Carol"];
names.Add("Dave");
// names.Add(42); // Compile-time error: can't add an int to List<string>

// A dictionary mapping string keys to int values
var inventory = new Dictionary<string, int>
{
    ["Apples"] = 50,
    ["Oranges"] = 30
};
inventory["Bananas"] = 25;

// A set that prevents duplicates
HashSet<int> uniqueIds = [1, 2, 3, 1, 2];
Console.WriteLine($"Unique count: {uniqueIds.Count}"); // 3

// A FIFO queue
Queue<string> tasks = new();
tasks.Enqueue("Build");
tasks.Enqueue("Test");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Build

Generiska samlingar förhindrar typfel under körningstid eftersom felen uppstår vid kompilering i stället. Dessa samlingar undviker också boxning för värdetyper, vilket förbättrar prestandan.

Allmänna metoder

En allmän metod deklarerar sin egen typparameter. Kompilatorn härleder ofta typargumentet från de värden som du skickar, så du behöver inte ange det explicit:

static void Print<T>(T value) =>
    Console.WriteLine($"Value: {value}");

Print(42);        // Compiler infers T as int
Print("hello");   // Compiler infers T as string
Print(3.14);      // Compiler infers T as double

I anropet Print(42) härleder kompilatorn T som int från argumentet. Du kan skriva Print<int>(42) explicit, men typinferens håller koden renare.

Samlingsuttryck

Samlingsuttryck (C# 12) ger en koncis syntax för att skapa samlingar. Använd hakparenteser i stället för konstruktoranrop eller initieringssyntax:

// Create a list with a collection expression
List<string> fruits = ["Apple", "Banana", "Cherry"];

// Create an array
int[] numbers = [1, 2, 3, 4, 5];

// Works with any supported collection type
IReadOnlyList<double> temperatures = [72.0, 68.5, 75.3];

Console.WriteLine($"Fruits: {string.Join(", ", fruits)}");
Console.WriteLine($"Numbers: {string.Join(", ", numbers)}");
Console.WriteLine($"Temps: {string.Join(", ", temperatures)}");

Spridningsoperatorn (..) infogar elementen i en samling till en annan, vilket är användbart för att kombinera sekvenser:

List<int> first = [1, 2, 3];
List<int> second = [4, 5, 6];

// Spread both lists into a new combined list
List<int> combined = [.. first, .. second];
Console.WriteLine(string.Join(", ", combined));
// Output: 1, 2, 3, 4, 5, 6

// Add extra elements alongside spreads
List<int> withExtras = [0, .. first, 99, .. second];
Console.WriteLine(string.Join(", ", withExtras));
// Output: 0, 1, 2, 3, 99, 4, 5, 6

Samlingsuttryck fungerar med matriser, List<T>, Span<T>, ImmutableArray<T>och alla typer som stöder samlingsverktygets mönster. Fullständig syntaxreferens finns i Samlingsuttryck.

Ordboksinitiering

Du kan initiera ordlistor kortfattat med indexerarinitierare. Den här syntaxen använder hakparenteser för att ange nyckel/värde-par:

Dictionary<string, int> scores = new()
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Carol"] = 92
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

Du kan sammanfoga ordlistor genom att kopiera en och tillämpa åsidosättningar:

Dictionary<string, int> defaults = new()
{
    ["Timeout"] = 30,
    ["Retries"] = 3
};
Dictionary<string, int> overrides = new()
{
    ["Timeout"] = 60
};

// Merge defaults and overrides into a new dictionary
Dictionary<string, int> config = new(defaults);
foreach (var (key, value) in overrides)
{
    config[key] = value;
}

Console.WriteLine($"Timeout: {config["Timeout"]}");  // 60
Console.WriteLine($"Retries: {config["Retries"]}");   // 3

Typbegränsningar

Begränsningar begränsar vilka typargument som en allmän typ eller metod accepterar. Med begränsningar kan du anropa metoder eller komma åt egenskaper för typparametern som inte skulle vara tillgängliga på enbart object.

static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

static T CreateDefault<T>() where T : new() => new T();

var list = CreateDefault<List<int>>(); // Creates an empty List<int>
Console.WriteLine($"Empty list count: {list.Count}"); // 0

De vanligaste begränsningarna är:

Begränsning Innebörd
where T : class T måste vara en referenstyp
where T : struct T måste vara en icke-nullbar värdetyp
where T : new() T måste ha en offentlig parameterlös konstruktor
where T : BaseClass T måste härledas från BaseClass
where T : IInterface T måste implementera IInterface

Du kan kombinera begränsningar: where T : class, IComparable<T>, new(). Mindre vanliga begränsningar är where T : System.Enum, where T : System.Delegateoch where T : unmanaged för specialiserade scenarier. Den fullständiga listan finns i Begränsningar för typparametrar.

Kovarians och kontravarians

Kovarians och kontravarians beskriver hur generiska typer beter sig med arv. De avgör om du kan använda ett argument av mer härledd eller mindre härledd typ än vad som ursprungligen angavs:

// Covariance: IEnumerable<Dog> can be used as IEnumerable<Animal>
// because IEnumerable<out T> is covariant
List<Dog> dogs = [new("Rex"), new("Buddy")];
IEnumerable<Animal> animals = dogs; // Allowed because Dog derives from Animal

foreach (var animal in animals)
{
    Console.WriteLine(animal.Name);
}

// Contravariance: Action<Animal> can be used as Action<Dog>
// because Action<in T> is contravariant
Action<Animal> printAnimal = a => Console.WriteLine($"Animal: {a.Name}");
Action<Dog> printDog = printAnimal; // Allowed because any Animal handler can handle Dog

printDog(new Dog("Spot"));
  • Covariance (out T): En IEnumerable<Dog> kan användas där IEnumerable<Animal> förväntas eftersom Dog härleds från Animal. Nyckelordet out för typparametern aktiverar detta. Parametrar av typen Covariant kan bara visas i utdatapositioner (returtyper).
  • Contravariance (in T): En Action<Animal> kan användas där Action<Dog> förväntas eftersom alla åtgärder som hanterar Animal också kan hantera Dog. Nyckelordet in aktiverar detta. Parametrar av typen Contravariant kan bara visas i indatapositioner (parametrar).

Många inbyggda gränssnitt och delegeringar är redan variant: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>, och Action<in T>. Du drar nytta av variansen automatiskt när du arbetar med dessa typer. En djupgående behandling av utformning av varianter av gränssnitt och delegater finns i Covariance och contravariance.

Skapa egna generiska typer

Du kan definiera dina egna allmänna klasser, structs, gränssnitt och metoder. I följande exempel visas en enkel allmän länkad lista som illustration. I praktiken använder du List<T> eller någon annan inbyggd samling:

public class GenericList<T>
{
    private class Node(T data)
    {
        public T Data { get; set; } = data;
        public Node? Next { get; set; }
    }

    private Node? head;

    public void AddHead(T data)
    {
        var node = new Node(data) { Next = head };
        head = node;
    }

    public IEnumerator<T> GetEnumerator()
    {
        var current = head;
        while (current is not null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}
var list = new GenericList<int>();
for (var i = 0; i < 5; i++)
{
    list.AddHead(i);
}

foreach (var item in list)
{
    Console.Write($"{item} ");
}
Console.WriteLine();
// Output: 4 3 2 1 0

Generiska typer är inte begränsade till klasser. Du kan definiera generiska interface, structoch record typer. Mer information om hur du utformar generiska algoritmer och komplexa begränsningskombinationer finns i Generics i .NET.

Se även