System typów języka C#

C# jest silnie typizowanego języka. Każda zmienna i stała ma typ, podobnie jak każde wyrażenie, które oblicza wartość. Każda deklaracja metody określa nazwę, typ i rodzaj (wartość, odwołanie lub dane wyjściowe) dla każdego parametru wejściowego i dla wartości zwracanej. Biblioteka klas platformy .NET definiuje wbudowane typy liczbowe i typy złożone, które reprezentują szeroką gamę konstrukcji. Obejmują one system plików, połączenia sieciowe, kolekcje i tablice obiektów oraz daty. Typowy program w języku C# używa typów z biblioteki klas i typów zdefiniowanych przez użytkownika, które modelują pojęcia specyficzne dla domeny problemu programu.

Informacje przechowywane w typie mogą zawierać następujące elementy:

  • Miejsce do magazynowania wymagane przez zmienną typu.
  • Maksymalna i minimalna wartość, którą może reprezentować.
  • Elementy członkowskie (metody, pola, zdarzenia itd.), które zawiera.
  • Typ podstawowy dziedziczy.
  • Interfejsy, które implementują.
  • Rodzaje dozwolonych operacji.

Kompilator używa informacji o typie, aby upewnić się, że wszystkie operacje wykonywane w kodzie są bezpieczne. Jeśli na przykład zadeklarujesz zmienną typu int, kompilator umożliwia używanie zmiennej w ramach operacji dodawania i odejmowania. Jeśli spróbujesz wykonać te same operacje na zmiennej typu bool, kompilator generuje błąd, jak pokazano w poniższym przykładzie:

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;

Uwaga

Deweloperzy języka C i C++ zauważają, że w języku C# bool nie jest konwertowany na int.

Kompilator osadza informacje o typie w pliku wykonywalny jako metadane. Środowisko uruchomieniowe języka wspólnego (CLR) używa tych metadanych w czasie wykonywania, aby dodatkowo zagwarantować bezpieczeństwo typu podczas przydzielania i odzyskiwania pamięci.

Określanie typów w deklaracjach zmiennych

Podczas deklarowania zmiennej lub stałej w programie należy określić jego typ lub użyć słowa kluczowego var , aby kompilator wywnioskować typ. W poniższym przykładzie przedstawiono niektóre deklaracje zmiennych, które używają wbudowanych typów liczbowych i złożonych typów zdefiniowanych przez użytkownika:

// 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;

Typy parametrów metody i wartości zwracanych są określone w deklaracji metody. Poniższy podpis przedstawia metodę, która wymaga int argumentu wejściowego i zwraca ciąg:

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

Po zadeklarowaniu zmiennej nie można ponownie zadeklarować jej nowego typu i nie można przypisać wartości niezgodnej z jego zadeklarowanym typem. Na przykład nie można zadeklarować wartościint, a następnie przypisać jej wartość logiczną .true Wartości można jednak przekonwertować na inne typy, na przykład po przypisaniu ich do nowych zmiennych lub przekazaniu jako argumentów metody. Konwersja typu, która nie powoduje utraty danych, jest wykonywana automatycznie przez kompilator. Konwersja, która może spowodować utratę danych, wymaga rzutu w kodzie źródłowym.

Aby uzyskać więcej informacji, zobacz Konwersje rzutów i typów.

Typy wbudowane

Język C# udostępnia standardowy zestaw wbudowanych typów. Reprezentują one liczby całkowite, wartości zmiennoprzecinkowe, wyrażenia logiczne, znaki tekstowe, wartości dziesiętne i inne typy danych. Istnieją również wbudowane string typy i object . Te typy są dostępne do użycia w dowolnym programie języka C#. Aby uzyskać pełną listę wbudowanych typów, zobacz Typy wbudowane.

Typy niestandardowe

structUżywasz konstrukcji , , class, enuminterfacei record do tworzenia własnych typów niestandardowych. Sama biblioteka klas platformy .NET jest kolekcją typów niestandardowych, których można używać we własnych aplikacjach. Domyślnie najczęściej używane typy w bibliotece klas są dostępne w dowolnym programie języka C#. Inne stają się dostępne tylko wtedy, gdy jawnie dodasz odwołanie do projektu do zestawu, który je definiuje. Gdy kompilator ma odwołanie do zestawu, można zadeklarować zmienne (i stałe) typów zadeklarowanych w tym zestawie w kodzie źródłowym. Aby uzyskać więcej informacji, zobacz Biblioteka klas platformy .NET.

Wspólny system typów

Ważne jest, aby zrozumieć dwa podstawowe kwestie dotyczące systemu typów na platformie .NET:

  • Obsługuje on zasadę dziedziczenia. Typy mogą pochodzić z innych typów nazywanych typami podstawowymi. Typ pochodny dziedziczy (z pewnymi ograniczeniami) metody, właściwości i inne elementy członkowskie typu podstawowego. Typ podstawowy może z kolei pochodzić z innego typu, w którym przypadku typ pochodny dziedziczy elementy członkowskie obu typów bazowych w hierarchii dziedziczenia. Wszystkie typy, w tym wbudowane typy liczbowe, takie jak System.Int32 (słowo kluczowe C#: int), pochodzą ostatecznie z jednego typu podstawowego, czyli System.Object (słowo kluczowe C#: object). Ta ujednolicona hierarchia typów nosi nazwę Common Type System (CTS). Aby uzyskać więcej informacji na temat dziedziczenia w języku C#, zobacz Dziedziczenie.
  • Każdy typ w usłudze CTS jest definiowany jako typ wartości lub typ odwołania. Te typy obejmują wszystkie typy niestandardowe w bibliotece klas platformy .NET, a także własne typy zdefiniowane przez użytkownika. Typy definiowane przy użyciu słowa kluczowego struct to typy wartości. Wszystkie wbudowane typy liczbowe to structs. Typy definiowane przy użyciu słowa kluczowego class lub record są typami referencyjnymi. Typy referencyjne i typy wartości mają różne reguły czasu kompilacji i różne zachowania czasu wykonywania.

Poniższa ilustracja przedstawia relację między typami wartości i typami referencyjnymi w usłudze CTS.

Screenshot that shows CTS value types and reference types.

Uwaga

Widać, że najczęściej używane typy są zorganizowane w System przestrzeni nazw. Jednak przestrzeń nazw, w której znajduje się typ, nie ma relacji z typem wartości lub typem odwołania.

Klasy i struktury to dwie podstawowe konstrukcje wspólnego systemu typów na platformie .NET. Język C# 9 dodaje rekordy, które są rodzajem klasy. Każda z nich jest zasadniczo strukturą danych, która hermetyzuje zestaw danych i zachowań, które należą razem jako jednostka logiczna. Dane i zachowania są członkami klasy, struktury lub rekordu. Elementy członkowskie zawierają metody, właściwości, zdarzenia itd., jak opisano w dalszej części tego artykułu.

Deklaracja klasy, struktury lub rekordu jest podobna do strategii używanej do tworzenia wystąpień lub obiektów w czasie wykonywania. W przypadku definiowania klasy, struktury lub rekordu o nazwie Person, Person jest nazwą typu. Jeśli deklarujesz i inicjujesz zmienną p typu Person, p mówi się, że jest to obiekt lub wystąpienie klasy Person. Można utworzyć wiele wystąpień tego samego Person typu, a każde wystąpienie może mieć różne wartości we właściwościach i polach.

Klasa jest typem referencyjnym. Po utworzeniu obiektu typu zmienna, do której przypisano obiekt, zawiera tylko odwołanie do tej pamięci. Gdy odwołanie do obiektu zostanie przypisane do nowej zmiennej, nowa zmienna odwołuje się do oryginalnego obiektu. Zmiany wprowadzone przez jedną zmienną są odzwierciedlane w innej zmiennej, ponieważ obie odnoszą się do tych samych danych.

Struktura jest typem wartości. Po utworzeniu struktury zmienna, do której przypisano strukturę, przechowuje rzeczywiste dane struktury. Gdy struktura zostanie przypisana do nowej zmiennej, zostanie skopiowana. Nowa zmienna i oryginalna zmienna zawierają zatem dwie oddzielne kopie tych samych danych. Zmiany wprowadzone w jednej kopii nie mają wpływu na drugą kopię.

Typy rekordów mogą być typami referencyjnymi (record class) lub typami wartości (record struct).

Ogólnie rzecz biorąc, klasy są używane do modelowania bardziej złożonego zachowania. Klasy zwykle przechowują dane, które mają być modyfikowane po utworzeniu obiektu klasy. Struktury najlepiej nadają się do małych struktur danych. Struktury zwykle przechowują dane, które nie mają być modyfikowane po utworzeniu struktury. Typy rekordów to struktury danych z dodatkowymi składowymi syntetyzowanymi kompilatorami. Rekordy zwykle przechowują dane, które nie mają być modyfikowane po utworzeniu obiektu.

Typy wartości

Typy wartości pochodzą z System.ValueTypeklasy , która pochodzi z System.Objectklasy . Typy pochodzące z System.ValueType tego typu mają specjalne zachowanie w środowisku CLR. Zmienne typu wartości zawierają bezpośrednio ich wartości. Pamięć dla struktury jest przydzielana w tekście w każdym kontekście zadeklarowanym przez zmienną. Nie ma oddzielnej alokacji sterty ani nakładu na odzyskiwanie pamięci dla zmiennych typu wartości. Można zadeklarować record struct typy, które są typami wartości i uwzględniać syntetyzowane elementy członkowskie dla rekordów.

Istnieją dwie kategorie typów wartości: struct i enum.

Wbudowane typy liczbowe są strukturami i mają pola i metody, do których można uzyskać dostęp:

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

Jednak deklarujesz i przypisujesz do nich wartości tak, jakby były prostymi typami niegregowanymi:

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

Typy wartości są zapieczętowane. Nie można utworzyć typu na podstawie dowolnego typu wartości, na przykład System.Int32. Nie można zdefiniować struktury dziedziczonej z żadnej klasy lub struktury zdefiniowanej przez użytkownika, ponieważ struktura może dziedziczyć tylko z System.ValueTypeklasy . Jednak struktura może implementować jeden lub więcej interfejsów. Można rzutować typ struktury na dowolny typ interfejsu, który implementuje. Rzutowanie powoduje, że operacja boxingu opakowuje strukturę wewnątrz obiektu typu odwołania na zarządzanym stosie. Operacje boxing są wykonywane podczas przekazywania typu wartości do metody, która przyjmuje System.Object typ interfejsu lub dowolnego typu jako parametr wejściowy. Aby uzyskać więcej informacji, zobacz Boxing and Unboxing (Boxing and Unboxing).

Słowo kluczowe struktury służy do tworzenia własnych niestandardowych typów wartości. Zazwyczaj struktura jest używana jako kontener dla małego zestawu powiązanych zmiennych, jak pokazano w poniższym przykładzie:

public struct Coords
{
    public int x, y;

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

Aby uzyskać więcej informacji na temat struktur, zobacz Typy struktury. Aby uzyskać więcej informacji na temat typów wartości, zobacz Typy wartości.

Drugą kategorią typów wartości jest enum. Wyliczenie definiuje zestaw nazwanych stałych całkowitych. Na przykład System.IO.FileMode wyliczenie w bibliotece klas platformy .NET zawiera zestaw nazwanych liczb całkowitych stałych określający sposób otwierania pliku. Jest on zdefiniowany tak, jak pokazano w poniższym przykładzie:

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

Stała System.IO.FileMode.Create ma wartość 2. Jednak nazwa jest znacznie bardziej zrozumiała dla ludzi odczytujących kod źródłowy, a z tego powodu lepiej używać wyliczenia zamiast stałych liczb literałów. Aby uzyskać więcej informacji, zobacz System.IO.FileMode.

Wszystkie wyliczenia dziedziczą z System.Enumelementu , który dziedziczy z System.ValueTypeelementu . Wszystkie reguły, które mają zastosowanie do struktur, mają również zastosowanie do wyliczenia. Aby uzyskać więcej informacji na temat wyliczenia, zobacz Typy wyliczenia.

Typy odwołań

Typ zdefiniowany jako class, , delegaterecord, tablica lub interface jest .reference type

Podczas deklarowania zmiennej reference typeelementu , zawiera ona wartość null do momentu przypisania jej z wystąpieniem tego typu lub utworzenia zmiennej new przy użyciu operatora . Tworzenie i przypisywanie klasy przedstawiono w poniższym przykładzie:

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

Nie interface można bezpośrednio utworzyć wystąpienia obiektu przy użyciu new operatora . Zamiast tego utwórz i przypisz wystąpienie klasy, która implementuje interfejs. Rozpatrzmy następujący przykład:

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();

Po utworzeniu obiektu pamięć jest przydzielana na zarządzanym stosie. Zmienna zawiera tylko odwołanie do lokalizacji obiektu. Typy na zarządzanym stercie wymagają narzuty zarówno po ich przydzieleniu, jak i odzyskiwaniu. Odzyskiwanie pamięci to funkcja automatycznego zarządzania pamięcią środowiska CLR, która wykonuje odzyskiwanie. Jednak odzyskiwanie pamięci jest również wysoce zoptymalizowane, a w większości scenariuszy nie powoduje problemu z wydajnością. Aby uzyskać więcej informacji na temat odzyskiwania pamięci, zobacz Automatyczne zarządzanie pamięcią.

Wszystkie tablice są typami referencyjnymi, nawet jeśli ich elementy są typami wartości. Tablice niejawnie pochodzą z System.Array klasy . Można je zadeklarować i używać z uproszczoną składnią dostarczaną przez język C#, jak pokazano w poniższym przykładzie:

// 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;

Typy referencyjne w pełni obsługują dziedziczenie. Podczas tworzenia klasy można dziedziczyć z dowolnego innego interfejsu lub klasy, która nie jest zdefiniowana jako zapieczętowana. Inne klasy mogą dziedziczyć z klasy i przesłaniać metody wirtualne. Aby uzyskać więcej informacji na temat tworzenia własnych klas, zobacz Klasy, struktury i rekordy. Aby uzyskać więcej informacji na temat dziedziczenia i metod wirtualnych, zobacz Dziedziczenie.

Typy wartości literałów

W języku C# wartości literału otrzymują typ z kompilatora. Możesz określić sposób wpisywania literału liczbowego, dołączając literę na końcu liczby. Na przykład, aby określić, że wartość 4.56 powinna być traktowana jako float, dołącz "f" lub "F" po liczbie: 4.56f. Jeśli litera nie zostanie dołączona, kompilator wywnioskuje typ literału. Aby uzyskać więcej informacji na temat typów, które można określić z sufiksami liter, zobacz Typy liczb całkowitych i Typy liczb zmiennoprzecinkowe.

Ponieważ literały są typizowane, a wszystkie typy pochodzą ostatecznie z System.Objectklasy , można napisać i skompilować kod, taki jak następujący 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);

Typy ogólne

Typ można zadeklarować przy użyciu co najmniej jednego parametru typu , który służy jako symbol zastępczy rzeczywistego typu ( typ konkretny). Kod klienta udostępnia konkretny typ podczas tworzenia wystąpienia typu. Takie typy są nazywane typami ogólnymi. Na przykład typ System.Collections.Generic.List<T> platformy .NET ma jeden parametr typu, który zgodnie z konwencją ma nazwę T. Podczas tworzenia wystąpienia typu należy określić typ obiektów, które będą zawierać lista, na przykład string:

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

Użycie parametru typu umożliwia ponowne użycie tej samej klasy do przechowywania dowolnego typu elementu bez konieczności konwertowania każdego elementu na obiekt. Klasy kolekcji ogólnych są nazywane silnie typiowanymi kolekcjami , ponieważ kompilator zna określony typ elementów kolekcji i może zgłosić błąd w czasie kompilacji, jeśli na przykład spróbujesz dodać liczbę całkowitą do stringList obiektu w poprzednim przykładzie. Aby uzyskać więcej informacji, zobacz Generics (Typy ogólne).

Typy niejawne, typy anonimowe i typy wartości dopuszczalnych do wartości null

Możesz niejawnie wpisać zmienną lokalną (ale nie składową klasy) przy użyciu słowa kluczowego var . Zmienna nadal otrzymuje typ w czasie kompilacji, ale typ jest dostarczany przez kompilator. Aby uzyskać więcej informacji, zobacz Niejawnie wpisane zmienne lokalne.

Może to być niewygodne, aby utworzyć nazwany typ dla prostych zestawów powiązanych wartości, których nie zamierzasz przechowywać ani przekazywać poza granice metody. W tym celu można tworzyć typy anonimowe . Aby uzyskać więcej informacji, zobacz Typy anonimowe.

Zwykłe typy wartości nie mogą mieć wartości null. Można jednak utworzyć typy wartości dopuszczających wartość null , dołączając wartość ? po typie. Na przykład jest typemint, int? który może również mieć wartość null. Typy wartości dopuszczalnych wartości są wystąpieniami ogólnego typu System.Nullable<T>struktury . Typy wartości dopuszczających wartość null są szczególnie przydatne podczas przekazywania danych do i z baz danych, w których wartości liczbowe mogą być null. Aby uzyskać więcej informacji, zobacz Typy wartości dopuszczalnych wartości null.

Typ czasu kompilacji i typ czasu wykonywania

Zmienna może mieć różne typy czasu kompilacji i czasu wykonywania. Typ czasu kompilacji jest zadeklarowanym lub wnioskowanym typem zmiennej w kodzie źródłowym. Typ czasu wykonywania jest typem wystąpienia, do którego odwołuje się ta zmienna. Często te dwa typy są takie same, jak w poniższym przykładzie:

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

W innych przypadkach typ czasu kompilacji jest inny, jak pokazano w następujących dwóch przykładach:

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

W obu powyższych przykładach typ czasu wykonywania to string. Typ czasu kompilacji znajduje się object w pierwszym wierszu i IEnumerable<char> w drugim.

Jeśli dwa typy są inne dla zmiennej, ważne jest, aby zrozumieć, kiedy typ czasu kompilacji i typ czasu wykonywania mają zastosowanie. Typ czasu kompilacji określa wszystkie akcje wykonywane przez kompilator. Te akcje kompilatora obejmują rozpoznawanie wywołań metod, rozpoznawanie przeciążeń oraz dostępne niejawne i jawne rzutowania. Typ czasu wykonywania określa wszystkie akcje, które są rozwiązywane w czasie wykonywania. Te akcje czasu wykonywania obejmują wysyłanie wywołań metody wirtualnej, ocenianie is i switch wyrażenia oraz inne interfejsy API testowania typów. Aby lepiej zrozumieć sposób interakcji kodu z typami, należy rozpoznać, która akcja ma zastosowanie do jakiego typu.

Aby uzyskać więcej informacji, zobacz następujące artykuły:

specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz Specyfikacja języka C#. Specyfikacja języka jest ostatecznym źródłem informacji o składni i użyciu języka C#.