C# Language
Typ wartości a typ odniesienia
Szukaj…
Składnia
- Przekazywanie przez odniesienie: public void Double (ref int numberToDouble) {}
Uwagi
Wprowadzenie
Typy wartości
Typy wartości są prostsze z tych dwóch. Typy wartości są często używane do reprezentowania samych danych. Liczba całkowita, wartość logiczna lub punkt w przestrzeni 3D to przykłady dobrych typów wartości.
Typy wartości (struktury) deklarowane są za pomocą słowa kluczowego struct. Zobacz sekcję dotyczącą składni, aby zobaczyć, jak zadeklarować nową strukturę.
Ogólnie rzecz biorąc, mamy 2 słowa kluczowe, które służą do deklarowania typów wartości:
- Struktury
- Wyliczenia
Typy referencyjne
Typy referencyjne są nieco bardziej złożone. Typy referencyjne są tradycyjnymi obiektami w znaczeniu programowania obiektowego. Wspierają więc dziedziczenie (i wynikające z niego korzyści), a także wspierają finalizatorów.
W języku C # ogólnie mamy następujące typy referencji:
- Klasy
- Delegaci
- Interfejsy
Nowe typy referencyjne (klasy) deklaruje się za pomocą słowa kluczowego class. Na przykład zobacz sekcję składni, aby dowiedzieć się, jak zadeklarować nowy typ odwołania.
Główne różnice
Główne różnice między typami odniesienia i typami wartości można zobaczyć poniżej.
Na stosie istnieją typy wartości, na stercie typy referencji
Jest to często wspomniana różnica między tymi dwoma, ale tak naprawdę sprowadza się to do tego, że gdy użyjesz typu wartości w C #, takiego jak int, program użyje tej zmiennej, aby odnieść się bezpośrednio do tej wartości. Jeśli powiesz int mine = 0, to zmienna mine odnosi się bezpośrednio do 0, co jest wydajne. Jednak typy referencji faktycznie zawierają (jak sama nazwa wskazuje) odwołanie do obiektu bazowego, jest to podobne do wskaźników w innych językach, takich jak C ++.
Możesz nie zauważyć efektów tego natychmiast, ale efekty są, są potężne i subtelne. Zobacz przykład zmiany typów referencji w innym miejscu.
Ta różnica jest głównym powodem następujących innych różnic i warto ją poznać.
Typy wartości nie zmieniają się, gdy zmieniasz je w metodzie, podobnie jak typy referencyjne
Gdy typ wartości jest przekazywany do metody jako parametr, jeśli metoda w jakikolwiek sposób zmienia wartość, wartość nie ulega zmianie. Natomiast przekazanie typu referencyjnego do tej samej metody i zmiana go spowoduje zmianę obiektu bazowego, dzięki czemu inne rzeczy korzystające z tego samego obiektu będą miały nowo zmieniony obiekt, a nie jego oryginalną wartość.
Zobacz przykład typów wartości vs typy referencyjne w metodach, aby uzyskać więcej informacji.
Co jeśli chcę je zmienić?Po prostu przekaż je do metody za pomocą słowa kluczowego „ref”, a następnie przekaż ten obiekt przez odniesienie. Czyli to ten sam obiekt w pamięci. Wprowadzane modyfikacje będą przestrzegane. Zobacz przykład przechodzenia przez odniesienie na przykład.
Typy wartości nie mogą mieć wartości null, typy referencyjne mogą
W zasadzie, jak mówi, możesz przypisać null do typu odniesienia, co oznacza, że do przypisanej zmiennej nie można przypisać żadnego rzeczywistego obiektu. W przypadku typów wartości nie jest to jednak możliwe. Możesz jednak użyć wartości Nullable, aby zezwolić na określenie wartości typu nullable, jeśli jest to wymóg, ale jeśli rozważasz to, zastanów się, czy klasa nie jest najlepszym podejściem, jeśli jest to Twoja własność rodzaj.
Zmieniamy wartości gdzie indziej
public static void Main(string[] args)
{
var studentList = new List<Student>();
studentList.Add(new Student("Scott", "Nuke"));
studentList.Add(new Student("Vincent", "King"));
studentList.Add(new Student("Craig", "Bertt"));
// make a separate list to print out later
var printingList = studentList; // this is a new list object, but holding the same student objects inside it
// oops, we've noticed typos in the names, so we fix those
studentList[0].LastName = "Duke";
studentList[1].LastName = "Kong";
studentList[2].LastName = "Brett";
// okay, we now print the list
PrintPrintingList(printingList);
}
private static void PrintPrintingList(List<Student> students)
{
foreach (Student student in students)
{
Console.WriteLine(string.Format("{0} {1}", student.FirstName, student.LastName));
}
}
Zauważysz, że chociaż lista printList została utworzona przed poprawkami nazwisk uczniów po literówkach, metoda PrintPrintingList nadal drukuje poprawione nazwy:
Scott Duke
Vincent Kong
Craig Brett
Wynika to z faktu, że obie listy zawierają listę odniesień do tych samych studentów. Tak więc zmiana bazowego obiektu ucznia podpowiada użycie według dowolnej listy.
Oto jak wyglądałaby klasa uczniów.
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Student(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
}
Przekazywanie przez odniesienie
Jeśli chcesz, aby przykład Typy wartości a Typy referencyjne w metodach działał poprawnie, użyj słowa kluczowego ref w podpisie metody dla parametru, który chcesz przekazać przez referencję, a także podczas wywoływania metody.
public static void Main(string[] args)
{
...
DoubleNumber(ref number); // calling code
Console.WriteLine(number); // outputs 8
...
}
public void DoubleNumber(ref int number)
{
number += number;
}
Wprowadzenie tych zmian spowodowałoby, że liczba byłaby aktualizowana zgodnie z oczekiwaniami, co oznacza, że wyjście konsoli dla liczby wynosiłoby 8.
Przekazywanie przez referencję przy użyciu słowa kluczowego ref.
Z dokumentacji :
W języku C # argumenty mogą być przekazywane do parametrów albo przez wartość, albo przez odwołanie. Przekazywanie przez odwołanie umożliwia członom funkcji, metodom, właściwościom, indeksatorom, operatorom i konstruktorom zmianę wartości parametrów i utrzymanie tej zmiany w środowisku wywołującym. Aby przekazać parametr przez odwołanie, użyj słowa kluczowego
ref
lubout
.
Różnica między ref
i out
polega na tym out
że out
oznacza, że przekazany parametr musi zostać przypisany przed zakończeniem funkcji. W przeciwieństwie do parametrów przekazanych z ref
można zmienić lub pozostawić bez zmian.
using System;
class Program
{
static void Main(string[] args)
{
int a = 20;
Console.WriteLine("Inside Main - Before Callee: a = {0}", a);
Callee(a);
Console.WriteLine("Inside Main - After Callee: a = {0}", a);
Console.WriteLine("Inside Main - Before CalleeRef: a = {0}", a);
CalleeRef(ref a);
Console.WriteLine("Inside Main - After CalleeRef: a = {0}", a);
Console.WriteLine("Inside Main - Before CalleeOut: a = {0}", a);
CalleeOut(out a);
Console.WriteLine("Inside Main - After CalleeOut: a = {0}", a);
Console.ReadLine();
}
static void Callee(int a)
{
a = 5;
Console.WriteLine("Inside Callee a : {0}", a);
}
static void CalleeRef(ref int a)
{
a = 6;
Console.WriteLine("Inside CalleeRef a : {0}", a);
}
static void CalleeOut(out int a)
{
a = 7;
Console.WriteLine("Inside CalleeOut a : {0}", a);
}
}
Wyjście :
Inside Main - Before Callee: a = 20
Inside Callee a : 5
Inside Main - After Callee: a = 20
Inside Main - Before CalleeRef: a = 20
Inside CalleeRef a : 6
Inside Main - After CalleeRef: a = 6
Inside Main - Before CalleeOut: a = 6
Inside CalleeOut a : 7
Inside Main - After CalleeOut: a = 7
Zadanie
var a = new List<int>();
var b = a;
a.Add(5);
Console.WriteLine(a.Count); // prints 1
Console.WriteLine(b.Count); // prints 1 as well
Przypisanie do zmiennej List<int>
nie powoduje utworzenia kopii List<int>
. Zamiast tego kopiuje odwołanie do List<int>
. Nazywamy typy, które zachowują się w ten sposób, typami referencyjnymi .
Różnica między parametrami metody ref i out
Istnieją dwa sposoby przekazania typu wartości przez odwołanie: ref
i out
. Różnica polega na tym, że przekazując ją z ref
wartość należy zainicjować, ale nie przy przekazywaniu jej out
. Użycie out
zapewnia, że zmienna ma wartość po wywołaniu metody:
public void ByRef(ref int value)
{
Console.WriteLine(nameof(ByRef) + value);
value += 4;
Console.WriteLine(nameof(ByRef) + value);
}
public void ByOut(out int value)
{
value += 4 // CS0269: Use of unassigned out parameter `value'
Console.WriteLine(nameof(ByOut) + value); // CS0269: Use of unassigned out parameter `value'
value = 4;
Console.WriteLine(nameof(ByOut) + value);
}
public void TestOut()
{
int outValue1;
ByOut(out outValue1); // prints 4
int outValue2 = 10; // does not make any sense for out
ByOut(out outValue2); // prints 4
}
public void TestRef()
{
int refValue1;
ByRef(ref refValue1); // S0165 Use of unassigned local variable 'refValue'
int refValue2 = 0;
ByRef(ref refValue2); // prints 0 and 4
int refValue3 = 10;
ByRef(ref refValue3); // prints 10 and 14
}
Złap polega na tym, że użycie parametru out
must
zostać zainicjowane przed opuszczeniem metody, dlatego możliwa jest następująca metoda z ref
ale nie out
:
public void EmtyRef(bool condition, ref int value)
{
if (condition)
{
value += 10;
}
}
public void EmtyOut(bool condition, out int value)
{
if (condition)
{
value = 10;
}
} //CS0177: The out parameter 'value' must be assigned before control leaves the current method
Wynika to z faktu, że jeśli condition
się nie utrzymuje, value
zostaje nieprzypisana.
parametry ref a out
Kod
class Program
{
static void Main(string[] args)
{
int a = 20;
Console.WriteLine("Inside Main - Before Callee: a = {0}", a);
Callee(a);
Console.WriteLine("Inside Main - After Callee: a = {0}", a);
Console.WriteLine();
Console.WriteLine("Inside Main - Before CalleeRef: a = {0}", a);
CalleeRef(ref a);
Console.WriteLine("Inside Main - After CalleeRef: a = {0}", a);
Console.WriteLine();
Console.WriteLine("Inside Main - Before CalleeOut: a = {0}", a);
CalleeOut(out a);
Console.WriteLine("Inside Main - After CalleeOut: a = {0}", a);
Console.ReadLine();
}
static void Callee(int a)
{
a += 5;
Console.WriteLine("Inside Callee a : {0}", a);
}
static void CalleeRef(ref int a)
{
a += 10;
Console.WriteLine("Inside CalleeRef a : {0}", a);
}
static void CalleeOut(out int a)
{
// can't use a+=15 since for this method 'a' is not intialized only declared in the method declaration
a = 25; //has to be initialized
Console.WriteLine("Inside CalleeOut a : {0}", a);
}
}
Wynik
Inside Main - Before Callee: a = 20
Inside Callee a : 25
Inside Main - After Callee: a = 20
Inside Main - Before CalleeRef: a = 20
Inside CalleeRef a : 30
Inside Main - After CalleeRef: a = 30
Inside Main - Before CalleeOut: a = 30
Inside CalleeOut a : 25
Inside Main - After CalleeOut: a = 25