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. Varje metoddeklaration anger ett namn, typ och typ (värde, referens eller utdata) för varje indataparameter och för returvärdet. .NET-klassbiblioteket definierar inbyggda numeriska typer och komplexa typer som representerar en mängd olika konstruktioner. Dessa 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.

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;

Typerna av metodparametrar och returvärden anges 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 inte är kompatibelt med dess deklarerade typ. Du kan till exempel inte deklarera en int och sedan tilldela den ett booleskt värde på true. Värden kan dock konverteras till andra typer, till exempel när de tilldelas till nya variabler eller skickas som metodargument. En typkonvertering som inte orsakar dataförlust utförs automatiskt av kompilatorn. 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 representerar heltal, flyttalsvärden, booleska uttryck, texttecken, decimalvärden och andra typer av data. Det finns också inbyggda string och object typer. Dessa typer är tillgängliga för dig att använda i valfritt C#-program. En fullständig lista över de inbyggda typerna finns i Inbyggda typer.

Anpassade typer

Du använder konstruktionerna struct, class, interface, enumoch record för att skapa 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. Andra blir bara tillgängliga när du uttryckligen lägger till en projektreferens till sammansättningen som definierar dem. När kompilatorn har en referens till sammansättningen kan du deklarera variabler (och konstanter) av de typer som deklareras i sammansättningen i källkoden. Mer information finns i .NET-klassbiblioteket.

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. Alternativen överlappar varandra. I de flesta scenarier är mer än ett alternativ ett rimligt val.

  • 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, inte 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.

Det vanliga typsystemet

Det är viktigt att förstå två grundläggande punkter om typsystemet i .NET:

  • Den 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 innehåller alla anpassade typer i .NET-klassbiblioteket och även dina egna användardefinierade typer. Typer som du definierar med nyckelordet struct är värdetyper. Alla inbyggda numeriska typer är structs. Typer som du definierar med hjälp av nyckelordet class eller record är referenstyper. Referenstyper och värdetyper har olika kompileringstidsregler och olika körningsbeteenden.

Följande bild visar relationen mellan värdetyper och referenstyper i CTS.

Skärmbild som visar CTS-värdetyper och referenstyper.

Anmärkning

Du kan se att de vanligaste typerna är 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. Var och en är i princip 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 recorddeklaration är en ritning som används för att skapa instanser eller objekt under 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. Flera instanser av samma Person typ kan skapas och varje instans kan ha olika värden i sina egenskaper och fält.

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

En struct är en värdetyp. När en struct skapas, innehåller variabeln som structen tilldelas structens faktiska data. När structen tilldelas 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 görs 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änds klasser för att modellera mer komplexa beteenden. Klasser lagrar vanligtvis data som är avsedda att ändras när ett klassobjekt har skapats. Structs passar bäst för små datastrukturer. Structs lagrar vanligtvis data som inte är avsedda att ändras när structen har skapats. Recordtyper är datastrukturer med extra medlemmar som har syntetiserats av kompilatorn. Poster lagrar vanligtvis data som inte är avsedda att ändras när 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. Det finns ingen separat heapallokering eller skräpinsamlingskostnader för värdetypsvariabler. 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.

Du använder nyckelordet struct 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
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

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 definieras som en 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 operatorn new . Skapande och tilldelning av en klass visas i följande exempel:

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

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

MyClass myClass = new MyClass();

// 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 objektet skapas, allokeras minnet på den hanterade heap. 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 den förenklade syntax som tillhandahålls av C#, 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# får literalvärden en typ från kompilatorn. 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 ingen bokstav läggs till härleder kompilatorn en typ för literalen. För mer information om vilka typer som kan anges med bokstavssuffix, se avsnittet om Integrala numeriska typer och Flyttalsnumeriska typer.

Eftersom literaler skrivs och alla typer i slutändan 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

En typ kan deklareras 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. Sådana typer kallas generiska typer. Till exempel har .NET-typen System.Collections.Generic.List<T> en typparameter som enligt konventionen får namnet 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);

Användningen av typparametern gör det möjligt att återanvända samma klass för att lagra alla typer av element, utan att behöva konvertera varje element till objekt. Generiska samlingsklasser kallas starkt skrivna samlingar eftersom kompilatorn känner till den specifika typen av samlingens element och kan generera ett fel vid kompileringstillfället 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.

Implicita typer, anonyma typer och nullbara värdetyper

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

Det kan vara obekvämt att skapa en namngiven typ för enkla uppsättningar med relaterade värden som du inte tänker lagra eller passera utanför metodgränserna. Du kan skapa anonyma typer för detta ändamål. Mer information finns i Anonyma typer.

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).

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 vidtas av kompilatorn. 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.