Przekazywanie obiektów przez referencję lub wartość w C #

233

W języku C # zawsze myślałem, że zmienne nieprymitywne były przekazywane przez referencję, a prymitywne wartości przekazywane przez wartość.

Zatem przekazując do metody dowolny nieprymitywny obiekt, wszystko, co zrobiono obiektowi w metodzie, spowodowałoby przekazanie obiektu. (C # 101 rzeczy)

Zauważyłem jednak, że kiedy mija obiekt System.Drawing.Image, wydaje się, że tak nie jest? Jeśli przekażę obiekt system.drawing.image do innej metody i załaduję obraz na ten obiekt, to pozwól tej metodzie wyjść poza zakres i wróć do metody wywołującej, czy obraz nie jest ładowany na oryginalny obiekt?

Dlaczego to?

Michael
źródło
20
Wszystkie zmienne są domyślnie przekazywane w C #. Podajesz wartość referencji w przypadku typów referencji.
Andrew Barber

Odpowiedzi:

502

Obiekty w ogóle nie są przekazywane. Domyślnie argument jest oceniany, a jego wartość przekazywana jest według wartości jako wartość początkowa parametru metody, którą wywołujesz. Ważną kwestią jest to, że wartość jest odwołaniem do typów odwołań - sposobem dotarcia do obiektu (lub wartości null). Zmiany w tym obiekcie będą widoczne dla osoby dzwoniącej. Jednak zmiana wartości parametru tak, aby odnosiła się do innego obiektu, nie będzie widoczna, gdy używasz wartości przekazywanej, która jest domyślna dla wszystkich typów.

Jeśli chcesz korzystać z funkcji przekazywania według odwołania, musisz użyć outlub ref, niezależnie od tego, czy typ parametru jest typem wartości, czy typem odniesienia. W takim przypadku sama zmienna jest przekazywana przez odwołanie, więc parametr używa tej samej lokalizacji pamięci co argument - a zmiany w samym parametrze są widoczne dla wywołującego.

Więc:

public void Foo(Image image)
{
    // This change won't be seen by the caller: it's changing the value
    // of the parameter.
    image = Image.FromStream(...);
}

public void Foo(ref Image image)
{
    // This change *will* be seen by the caller: it's changing the value
    // of the parameter, but we're using pass by reference
    image = Image.FromStream(...);
}

public void Foo(Image image)
{
    // This change *will* be seen by the caller: it's changing the data
    // within the object that the parameter value refers to.
    image.RotateFlip(...);
}

Mam artykuł, który zawiera w tym dużo więcej szczegółów . Zasadniczo „przekazanie przez odniesienie” nie oznacza, co według ciebie oznacza.

Jon Skeet
źródło
2
Masz rację, nie widziałem tego! I ładowanie obrazu = Image.FromFile (..) i to zastępowało zmienny obraz i nie zmieniało obiektu! :) oczywiście.
Michael
1
@Adeem: Niezupełnie - nie ma „obiektu parametru”, jest obiekt, do którego odnosi się wartość parametru. Myślę, że masz dobry pomysł, ale terminologia ma znaczenie :)
Jon Skeet
2
Gdybyśmy upuść słów kluczowych refi outod C #, to jest ok, aby powiedzieć, że C # przekazuje parametry w taki sam sposób jak Java czy to znaczy zawsze przez wartość. Czy jest jakaś różnica w java?
Internet szerokopasmowy
1
@broadband: Tak, domyślnym trybem przekazywania jest wartość domyślna. Chociaż oczywiście C # ma wskaźniki i niestandardowe typy wartości, co sprawia, że ​​wszystko jest nieco bardziej skomplikowane niż w Javie.
Jon Skeet
3
@Vippy: Nie, wcale nie. To kopia referencji . Proponuję przeczytać link do artykułu.
Jon Skeet,
18

Jeszcze jeden przykład kodu, aby to pokazać:

void Main()
{


    int k = 0;
    TestPlain(k);
    Console.WriteLine("TestPlain:" + k);

    TestRef(ref k);
    Console.WriteLine("TestRef:" + k);

    string t = "test";

    TestObjPlain(t);
    Console.WriteLine("TestObjPlain:" +t);

    TestObjRef(ref t);
    Console.WriteLine("TestObjRef:" + t);
}

public static void TestPlain(int i)
{
    i = 5;
}

public static void TestRef(ref int i)
{
    i = 5;
}

public static void TestObjPlain(string s)
{
    s = "TestObjPlain";
}

public static void TestObjRef(ref string s)
{
    s = "TestObjRef";
}

A wynik:

TestPlain: 0

TestRef: 5

TestObjPlain: test

TestObjRef: TestObjRef

vmg
źródło
2
Więc w zasadzie typ referencyjny nadal POTRZEBUJE BYĆ PRZEKAZANY jako referencja, jeśli chcemy zobaczyć zmiany w funkcji dzwoniącego.
Niezniszczalny
1
Ciągi to niezmienne typy referencyjne. Niezmienny oznacza, że ​​nie można go zmienić po jego utworzeniu. Każda zmiana ciągu spowoduje utworzenie nowego ciągu. Dlatego ciągi musiały zostać przekazane jako „ref”, aby uzyskać zmianę w metodzie wywoływania. Inne obiekty (np. Pracownik) można przekazać bez „ref”, aby przywrócić zmiany w metodzie wywoływania.
Himalaya Garg
1
@vmg, jak na HimalayaGarg, nie jest to bardzo dobry przykład. Musisz podać inny przykład typu odniesienia, który nie jest niezmienny.
Daniel
11

Dodano wiele dobrych odpowiedzi. Nadal chcę przyczynić się, być może wyjaśni to nieco więcej.

Gdy przekazujesz instancję jako argument do metody, przekazuje copyinstancję. Teraz, jeśli przekazywana instancja jest value type(znajduje się w stack), przekazujesz kopię tej wartości, więc jeśli ją zmodyfikujesz, nie zostanie ona odzwierciedlona w obiekcie wywołującym. Jeśli instancja jest typem referencji, przekazujesz kopię referencji (ponownie znajduje się w stack) do obiektu. Masz więc dwa odniesienia do tego samego obiektu. Obaj mogą modyfikować obiekt. Ale jeśli w treści metody utworzysz nowy obiekt, twoja kopia odwołania nie będzie już odnosić się do oryginalnego obiektu, będzie odnosić się do nowo utworzonego obiektu. W rezultacie będziesz mieć 2 referencje i 2 obiekty.

OlegI
źródło
To powinna być wybrana odpowiedź!
JAN
Zgadzam się całkowicie! :)
JOSEFtw
8

Myślę, że jest to bardziej zrozumiałe, kiedy robisz to w ten sposób. Polecam pobranie LinqPad, aby przetestować takie rzeczy.

void Main()
{
    var Person = new Person(){FirstName = "Egli", LastName = "Becerra"};

    //Will update egli
    WontUpdate(Person);
    Console.WriteLine("WontUpdate");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");

    UpdateImplicitly(Person);
    Console.WriteLine("UpdateImplicitly");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");

    UpdateExplicitly(ref Person);
    Console.WriteLine("UpdateExplicitly");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");
}

//Class to test
public class Person{
    public string FirstName {get; set;}
    public string LastName {get; set;}

    public string printName(){
        return $"First name: {FirstName} Last name:{LastName}";
    }
}

public static void WontUpdate(Person p)
{
    //New instance does jack...
    var newP = new Person(){FirstName = p.FirstName, LastName = p.LastName};
    newP.FirstName = "Favio";
    newP.LastName = "Becerra";
}

public static void UpdateImplicitly(Person p)
{
    //Passing by reference implicitly
    p.FirstName = "Favio";
    p.LastName = "Becerra";
}

public static void UpdateExplicitly(ref Person p)
{
    //Again passing by reference explicitly (reduntant)
    p.FirstName = "Favio";
    p.LastName = "Becerra";
}

I to powinno dać wynik

WontUpdate

Imię: Egli, Nazwisko: Becerra

Aktualizuj niejawnie

Imię: Favio, Nazwisko: Becerra

UpdateExplicitly

Imię: Favio, Nazwisko: Becerra

Egli Becerra
źródło
a co z publiczną statyczną nieważnością WhatAbout (Person p) {p = new Person () {FirstName = "First", LastName = "Last"}; }. :)
Marin Popov
4

Gdy przekazujesz System.Drawing.Imageobiekt typu do metody, faktycznie przekazujesz kopię odwołania do tego obiektu.

Więc jeśli w tej metodzie ładujesz nowy obraz, ładujesz za pomocą nowego / skopiowanego odwołania. Nie wprowadzasz zmian w oryginale.

YourMethod(System.Drawing.Image image)
{
    //now this image is a new reference
    //if you load a new image 
    image = new Image()..
    //you are not changing the original reference you are just changing the copy of original reference
}
Haris Hasan
źródło
-1

W Pass By Reference dodajesz tylko „ref” do parametrów funkcji i jeszcze jedną rzecz, którą powinieneś zadeklarować jako „static”, ponieważ main jest static (# public void main(String[] args))!

namespace preparation
{
  public  class Program
    {
      public static void swap(ref int lhs,ref int rhs)
      {
          int temp = lhs;
          lhs = rhs;
          rhs = temp;
      }
          static void Main(string[] args)
        {
            int a = 10;
            int b = 80;

  Console.WriteLine("a is before sort " + a);
            Console.WriteLine("b is before sort " + b);
            swap(ref a, ref b);
            Console.WriteLine("");
            Console.WriteLine("a is after sort " + a);
            Console.WriteLine("b is after sort " + b);  
        }
    }
}
użytkownik5593590
źródło