Dela via


C#-typsystemet

C# är ett starkt skrivet språk. Varje variabel och konstant har en typ, liksom varje uttryck som utvärderas till ett värde. C# använder främst ett normativt typsystem. Ett normativt typsystem använder namn för att identifiera varje typ. I C#, struct, class, och interface -typer, inklusive record typer, identifieras alla med deras namn. Varje metoddeklaration anger ett namn, en typ och en typ (värde, referens eller utdata) för varje parameter och för returvärdet. .NET-klassbiblioteket definierar inbyggda numeriska typer och komplexa typer som representerar en mängd olika konstruktioner. Dessa konstruktioner omfattar filsystemet, nätverksanslutningar, samlingar och matriser med objekt och datum. Ett typiskt C#-program använder typer från klassbiblioteket och användardefinierade typer som modellerar de begrepp som är specifika för programmets problemdomän.

C# stöder också strukturella typer, till exempel tupplar och anonyma typer. Strukturella typer definieras av namnen och typerna för varje medlem och ordningen på medlemmar i ett uttryck. Strukturella typer har inte unika namn.

Informationen som lagras i en typ kan innehålla följande objekt:

  • Det lagringsutrymme som en variabel av typen kräver.
  • De högsta och lägsta värden som det kan representera.
  • De medlemmar (metoder, fält, händelser och så vidare) som de innehåller.
  • Den bastyp som den ärver från.
  • De gränssnitt som implementeras.
  • De åtgärder som tillåts.

Kompilatorn använder typinformation för att se till att alla åtgärder som utförs i koden är typsäkra. Om du till exempel deklarerar en variabel av typen intkan du med kompilatorn använda variabeln i additions- och subtraktionsåtgärder. Om du försöker utföra samma åtgärder på en variabel av typen boolgenererar kompilatorn ett fel, vilket visas i följande exempel:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Anmärkning

C- och C++-utvecklare, observera att i C#, bool inte kan konverteras till int.

Kompilatorn bäddar in typinformationen i den körbara filen som metadata. Common Language Runtime (CLR) använder dessa metadata vid körning för att ytterligare garantera typsäkerhet när den allokerar och återtar minne.

Ange typer i variabeldeklarationer

När du deklarerar en variabel eller konstant i ett program måste du antingen ange dess typ eller använda nyckelordet var för att låta kompilatorn härleda typen. I följande exempel visas några variabeldeklarationer som använder både inbyggda numeriska typer och komplexa användardefinierade typer:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = [0, 1, 2, 3, 4, 5];
var query = from item in source
            where item <= limit
            select item;

Du anger typerna av metodparametrar och returnerar värden i metoddeklarationen. Följande signatur visar en metod som kräver ett int som indataargument och returnerar en sträng:

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = ["Spencer", "Sally", "Doug"];

När du har deklarerat en variabel kan du inte omdeklarerat den med en ny typ och du kan inte tilldela ett värde som är inkompatibelt med dess deklarerade typ. Du kan till exempel inte deklarera en int och sedan tilldela den ett booleskt värde på true. Du kan dock konvertera värden till andra typer, till exempel när du tilldelar dem till nya variabler eller skickar dem som metodargument. Kompilatorn utför automatiskt en typkonvertering som inte orsakar dataförlust. En konvertering som kan orsaka dataförlust kräver en cast i källkoden.

Mer information finns i Konvertering och typkonvertering.

Inbyggda typer

C# tillhandahåller en standarduppsättning med inbyggda typer. Dessa typer representerar heltal, flyttalsvärden, booleska uttryck, texttecken, decimalvärden och andra typer av data. Språket innehåller även inbyggda string och object typer. Du kan använda dessa typer i valfritt C#-program. En fullständig lista över de inbyggda typerna finns i Inbyggda typer.

Anpassade typer

Skapa strukturella typer med tupplar för att lagra relaterade datamedlemmar. Dessa typer ger en struktur som innehåller flera medlemmar. Tupplar har begränsat beteende. De är en behållare för värden. Det här är de enklaste typerna du kan skapa. Du kan senare bestämma att du behöver beteende. I så fall kan du konvertera en tupl till antingen en struct eller class.

Använd konstruktionerna struct, class, interface, enumoch record för att skapa dina egna anpassade typer. Själva .NET-klassbiblioteket är en samling anpassade typer som du kan använda i dina egna program. Som standard är de vanligaste typerna i klassbiblioteket tillgängliga i alla C#-program. Du gör andra typer tillgängliga genom att uttryckligen lägga till en paketreferens till paketet som tillhandahåller dem. När kompilatorn har en referens till paketet kan du deklarera variabler och konstanter för de typer som deklareras i paketets sammansättningar i källkoden.

Ett av de första besluten du fattar när du definierar en typ är att bestämma vilken konstruktion som ska användas för din typ. Följande lista hjälper dig att fatta det första beslutet. Vissa alternativ överlappar varandra. I de flesta scenarier är mer än ett alternativ ett rimligt val.

  • Om datatypen inte ingår i din appdomän och inte innehåller något beteende använder du en strukturell typ.
  • Om datalagringsstorleken är liten, högst 64 byte, väljer du en struct eller record struct.
  • Om typen är oföränderlig eller om du vill ha icke-förstörande mutation väljer du en struct eller record struct.
  • Om din typ ska ha värdesemantik för likhet väljer du en record class eller record struct.
  • Om typen främst används för att lagra data, med minimalt beteende, väljer du en record class eller record struct.
  • Om typen är en del av en arvshierarki väljer du en record class eller en class.
  • Om typen använder polymorfism väljer du en class.
  • Om det primära syftet är beteende väljer du en class.

Du kan också välja ett interface för att modellera ett kontrakt: beteende som beskrivs av medlemmar som kan implementeras av orelaterade typer. Gränssnitt är abstrakta och deklarerar medlemmar som måste implementeras av alla class eller struct typer som ärver från det gränssnittet.

Det vanliga typsystemet

Det gemensamma typsystemet stöder arvsprincipen. Typer kan härledas från andra typer, så kallade bastyper. Den härledda typen ärver (med vissa begränsningar) metoderna, egenskaperna och andra medlemmar av bastypen. Bastypen kan i sin tur härledas från någon annan typ, i vilket fall den härledda typen ärver medlemmarna i båda bastyperna i arvshierarkin.

Alla typer, inklusive inbyggda numeriska typer som System.Int32 (C#-nyckelord: int), härleds slutligen från en enda bastyp, vilket är System.Object (C#-nyckelord: object). Den här enhetliga typhierarkin kallas för Common Type System (CTS). Mer information om arv i C# finns i Arv.

Varje typ i CTS definieras som antingen en värdetyp eller en referenstyp. Dessa typer omfattar alla anpassade typer i .NET-klassbiblioteket och även dina egna användardefinierade typer:

  • Typer som du definierar med hjälp av nyckelorden struct eller record struct är värdetyper. Alla inbyggda numeriska typer är structs.
  • Typer som du definierar med hjälp av nyckelorden class, record classeller record är referenstyper.

Referenstyper och värdetyper har olika kompileringsregler och olika körningsbeteenden.

Anmärkning

De vanligaste typerna är alla ordnade i System namnområdet. Namnområdet där en typ finns har dock ingen relation till om det är en värdetyp eller referenstyp.

Klasser och structs är två av de grundläggande konstruktionerna i det gemensamma typsystemet i .NET. Varje konstruktion är i huvudsak en datastruktur som kapslar in en uppsättning data och beteenden som hör ihop som en logisk enhet. Data och beteenden är medlemmar i klassen, strukturen eller posten. Medlemskapen inkluderar dess metoder, egenskaper, händelser och så vidare, som kommer att listas senare i den här artikeln.

En klass-, struct- eller rekorddeklaration är som en ritning som du använder för att skapa instanser eller objekt vid körningstid. Om du definierar en klass, struct eller post med namnet Person, Person är namnet på typen. Om du deklarerar och initierar en variabel p av typen Personsägs p vara ett objekt eller en instans av Person. Du kan skapa flera instanser av samma Person typ och varje instans kan ha olika värden i dess egenskaper och fält.

En klass är en referenstyp. När du skapar ett objekt av typen innehåller variabeln som du tilldelar objektet endast en referens till det minnet. När du tilldelar objektreferensen till en ny variabel refererar den nya variabeln till det ursprungliga objektet. Ändringar som du gör via en variabel återspeglas i den andra variabeln eftersom de båda refererar till samma data.

En struct är en värdetyp. När du skapar en struct innehåller variabeln som du tilldelar structen structens faktiska data. När du tilldelar structen till en ny variabel kopieras den. Den nya variabeln och den ursprungliga variabeln innehåller därför två separata kopior av samma data. Ändringar som du gör i en kopia påverkar inte den andra kopian.

Posttyper kan vara antingen referenstyper (record class) eller värdetyper (record struct). Posttyper innehåller metoder som stöder värdejämlikhet.

I allmänhet använder du klasser för att modellera mer komplext beteende. Klasser lagrar vanligtvis data som du ändrar när ett klassobjekt har skapats. Structs passar bäst för små datastrukturer. Structs lagrar vanligtvis data som du inte ändrar när structen har skapats. Rekordtyper är datastrukturer med extra medlemmar genererade av kompilatorn. Register lagrar vanligtvis data som du inte ändrar efter att objektet har skapats.

Värdetyper

Värdetyper härleds från System.ValueType, som härleds från System.Object. Typer som härleds från System.ValueType har ett särskilt beteende i den gemensamma språkexekveringen (CLR). Värdetypsvariabler innehåller direkt deras värden. Minnet för en struct allokeras direkt i vilken kontext variabeln deklareras. Du kan deklarera record struct typer som är värdetyper och inkludera de syntetiserade medlemmarna för poster.

Det finns två kategorier av värdetyper: struct och enum.

De inbyggda numeriska typerna är structs och de har fält och metoder som du kan komma åt:

// constant field on type byte.
byte b = byte.MaxValue;

Men du deklarerar och tilldelar värden till dem som om de vore enkla icke-aggregerade typer:

byte num = 0xA;
int i = 5;
char c = 'Z';

Värdetyper är förseglade. Du kan inte härleda en typ från någon värdetyp, till exempel System.Int32. Du kan inte definiera en struct som ska ärvas från någon användardefinierad klass eller struct eftersom en struct bara kan ärva från System.ValueType. En struct kan dock implementera ett eller flera gränssnitt. Du kan omvandla en structtyp till vilken gränssnittstyp som helst som den implementerar. Denna typkonvertering gör att en boxningsåtgärd omsluter strukturen i ett referenstypobjekt i den hanterade heapen. Boxningsåtgärder utförs när du skickar en värdetyp till en metod som tar en System.Object eller någon gränssnittstyp som indataparameter. Mer information finns i Boxning och Avboxning.

Använd struct-nyckelordet för att skapa dina egna anpassade värdetyper. Vanligtvis används en struct som en container för en liten uppsättning relaterade variabler, som du ser i följande exempel:

public struct Coords(int x, int y)
{
    public int X { get; init; } = x;
    public int Y { get; init; } = y;
}

Mer information om structs finns i Strukturtyper. Mer information om värdetyper finns i Värdetyper.

Den andra kategorin av värdetyper är enum. En typ av enum definierar en uppsättning namngivna heltalkonstanter. Till exempel innehåller System.IO.FileMode uppräkning i .NET-klassbiblioteket en uppsättning namngivna konstanta heltal som anger hur en fil ska öppnas. Det definieras enligt följande exempel:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

Konstanten System.IO.FileMode.Create har värdet 2. Namnet är dock mycket mer meningsfullt för människor som läser källkoden, och därför är det bättre att använda uppräkningar i stället för konstanta literalnummer. Mer information finns i System.IO.FileMode.

Alla uppräkningar ärver från System.Enum, som ärver från System.ValueType. Alla regler som gäller för structs gäller även för uppräkningar. Mer information om uppräkningar finns i Uppräkningstyper.

Referenstyper

En typ som du definierar som en class, record class, record, delegate, matris eller interface är en reference type.

När du deklarerar en variabel för en reference typeinnehåller den värdet null tills du tilldelar den med en instans av den typen eller skapar en med hjälp av operatorn new . I följande exempel visas skapande och tilldelning av en klass:

MyClass myClass = new();
MyClass myClass2 = myClass;

Du kan inte direkt instansiera en interface med hjälp av operatorn new . Skapa och tilldela i stället en instans av en klass som implementerar gränssnittet. Tänk på följande exempel:

MyClass myClass = new();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

När du skapar objektet allokerar systemet minne på den hanterade heapen. Variabeln innehåller bara en referens till objektets plats. Typer på det hanterade heap-minnet medför omkostnader både när de allokeras och när de frigörs. Skräpinsamling är den automatiska minneshanteringsfunktionen i CLR, som utför återvinningen. Men skräpinsamlingen är också mycket optimerad och i de flesta fall skapar den inte något prestandaproblem. Mer information om skräpinsamling finns i Automatisk minneshantering.

Alla matriser är referenstyper, även om deras element är värdetyper. Matriser härleds implicit från System.Array klassen. Du deklarerar och använder dem med hjälp av den förenklade syntax som C# tillhandahåller, enligt följande exempel:

// Declare and initialize an array of integers.
int[] nums = [1, 2, 3, 4, 5];

// Access an instance property of System.Array.
int len = nums.Length;

Referenstyper har fullt stöd för arv. När du skapar en klass kan du ärva från andra gränssnitt eller klasser som inte har definierats som förseglade. Andra klasser kan ärva från din klass och åsidosätta dina virtuella metoder. För mer information om hur du skapar egna klasser kan du se Klasser, structs och records. Mer information om arv och virtuella metoder finns i Arv.

Typer av literalvärden

I C#tilldelar kompilatorn en typ till literalvärden. Du kan ange hur en numerisk literal ska skrivas genom att lägga till en bokstav i slutet av talet. Om du till exempel vill ange att värdet 4.56 ska behandlas som ett floatlägger du till ett "f" eller "F" efter talet: 4.56f. Om du inte lägger till en bokstav härleder kompilatorn en typ för literalen. Mer information om vilka typer du kan ange med bokstavssuffix finns i integrala numeriska typer och numeriska typer av flyttal.

Eftersom literaler skrivs och alla typer slutligen härleds från System.Objectkan du skriva och kompilera kod, till exempel följande kod:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Generiska typer

Deklarera en typ med en eller flera typparametrar som fungerar som platshållare för den faktiska typen ( betongtypen). Klientkoden tillhandahåller den konkreta typen när den skapar en instans av typen. Dessa typer kallas generiska typer. Till exempel har .NET-typen System.Collections.Generic.List<T> en typparameter som enligt konventionen heter T. När du skapar en instans av typen anger du typen av objekt som listan innehåller, till exempel string:

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

Med hjälp av typparametern kan du återanvända samma klass för att lagra alla typer av element, utan att behöva konvertera varje element till objekt. Generiska samlingsklasser är starkt skrivna samlingar eftersom kompilatorn känner till den specifika typen av samlingens element och kan generera ett fel vid kompileringstiden om du till exempel försöker lägga till ett heltal stringList i objektet i föregående exempel. Mer information finns i Generiska objekt.

Tupplar och anonyma typer

Det kan vara obekvämt att skapa en typ för enkla uppsättningar med relaterade värden om du inte tänker lagra eller skicka dessa värden med offentliga API:er. Du kan skapa tupplar eller anonyma typer för det här ändamålet. Mer information finns i tupplar och anonyma typer.

Typer av null-värden

Vanliga värdetyper kan inte ha värdet null. Du kan dock skapa null-värdetyper genom att lägga till en ? efter typen. Är till exempel int? en int typ som också kan ha värdet null. Nullbara värdetyper är instanser av den generiska structtypen System.Nullable<T>. Typer av null-värden är särskilt användbara när du skickar data till och från databaser där numeriska värden kan vara null. Mer information finns i Nullable value types (Ogiltiga värdetyper).

Implicita typdeklarationer

Skriv implicit en lokal variabel (men inte klassmedlemmar) med hjälp av nyckelordet var . Variabeln tar fortfarande emot en typ vid kompileringstillfället, men kompilatorn tillhandahåller typen . Mer information finns i Implicit inskrivna lokala variabler.

Kompileringstidstyp och körningstyp

En variabel kan ha olika kompileringstidstyper och körningstidstyper. Kompileringstidstypen är den deklarerade eller härledda typen av variabel i källkoden. Körningstidstypen är den typ av instans som den variabeln refererar till. Dessa två typer är ofta desamma, som i följande exempel:

string message = "This is a string of characters";

I andra fall skiljer sig kompileringstidstypen, vilket visas i följande två exempel:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

I båda de föregående exemplen är körningstidstypen en string. Kompileringstidstypen finns object på den första raden och IEnumerable<char> i den andra.

Om de två typerna skiljer sig åt för en variabel är det viktigt att förstå när kompileringstidstypen och körningstypen gäller. Kompileringstidstypen avgör alla åtgärder som kompilatorn vidtar. Dessa kompilatoråtgärder omfattar metodanropshantering, överbelastningshantering och tillgängliga implicita och explicita typkonverteringar. Körningstypen avgör alla åtgärder som löses vid körning. Dessa körningsåtgärder omfattar att skicka anrop till virtuella metoder, utvärdera is och switch uttryck samt andra API:er för typtestning. För att bättre förstå hur koden interagerar med typer kan du känna igen vilken åtgärd som gäller för vilken typ.

Mer information finns i följande artiklar:

Språkspecifikation för C#

Mer information finns i C#-språkspecifikationen. Språkspecifikationen är den slutgiltiga källan för C#-syntax och -användning.