Bagaimana string diteruskan dalam .NET?

121

Ketika saya meneruskan stringke suatu fungsi, apakah pointer ke konten string dilewatkan, atau seluruh string diteruskan ke fungsi di stack seperti a struct?

Cole Johnson
sumber

Jawaban:

278

Sebuah referensi dilewatkan; Namun, secara teknis itu tidak diteruskan oleh referensi. Ini adalah perbedaan yang halus, tetapi sangat penting. Perhatikan kode berikut:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Ada tiga hal yang perlu Anda ketahui untuk memahami apa yang terjadi di sini:

  1. String adalah tipe referensi di C #.
  2. Mereka juga tidak dapat diubah, jadi setiap kali Anda melakukan sesuatu yang sepertinya Anda mengubah string, Anda tidak melakukannya. Sebuah string yang benar-benar baru akan dibuat, referensi diarahkan ke sana, dan yang lama akan dibuang.
  3. Meskipun string adalah tipe referensi, strMaintidak diteruskan oleh referensi. Ini adalah tipe referensi, tetapi referensi itu sendiri diteruskan oleh nilai . Setiap kali Anda meneruskan parameter tanpa refkata kunci (tidak menghitung outparameter), Anda telah melewati sesuatu berdasarkan nilai.

Jadi itu berarti Anda ... memberikan referensi berdasarkan nilai. Karena ini adalah tipe referensi, hanya referensi yang disalin ke tumpukan. Tapi apa maksudnya itu?

Meneruskan jenis referensi berdasarkan nilai: Anda sudah melakukannya

Variabel C # adalah tipe referensi atau tipe nilai . Parameter C # bisa diteruskan dengan referensi atau diteruskan dengan nilai . Terminologi adalah masalah di sini; ini terdengar seperti hal yang sama, tetapi sebenarnya tidak.

Jika Anda meneruskan parameter jenis APA PUN, dan Anda tidak menggunakan refkata kunci, Anda telah meneruskannya menurut nilai. Jika Anda lulus berdasarkan nilai, yang sebenarnya Anda berikan adalah salinannya. Tetapi jika parameternya adalah tipe referensi, maka hal yang Anda salin adalah referensi tersebut, bukan apa pun yang ditunjukkannya.

Inilah baris pertama dari Mainmetode ini:

string strMain = "main";

Kami telah membuat dua hal di baris ini: string dengan nilai yang maindisimpan di memori di suatu tempat, dan variabel referensi yang disebut strMainmenunjuk ke sana.

DoSomething(strMain);

Sekarang kami meneruskan referensi itu ke DoSomething. Kami telah menyampaikannya berdasarkan nilai, jadi itu berarti kami membuat salinannya. Ini adalah tipe referensi, jadi itu berarti kami menyalin referensi, bukan string itu sendiri. Sekarang kita memiliki dua referensi yang masing-masing menunjuk ke nilai yang sama dalam memori.

Di dalam callee

Inilah metode teratas DoSomething:

void DoSomething(string strLocal)

Tidak ada refkata kunci, jadi strLocaldan strMainmerupakan dua referensi berbeda yang menunjuk pada nilai yang sama. Jika kita menetapkan kembali strLocal...

strLocal = "local";   

... kami belum mengubah nilai yang disimpan; kami mengambil referensi yang disebut strLocaldan mengarahkannya ke string baru. Apa yang terjadi strMainjika kita melakukan itu? Tidak ada. Itu masih menunjuk pada string lama.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Kekekalan

Mari kita ubah skenario sebentar. Bayangkan kita tidak bekerja dengan string, tetapi beberapa jenis referensi yang bisa berubah, seperti kelas yang Anda buat.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Jika Anda mengikuti referensi objLocalke objek yang dituju, Anda dapat mengubah propertinya:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

Masih hanya ada satu MutableThingdalam memori, dan referensi yang disalin dan referensi asli masih mengarah padanya. Properti MutableThingitu sendiri telah berubah :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

Ah, tapi string tidak bisa diubah! Tidak ada ChangeMeproperti untuk disetel. Anda tidak dapat melakukannya strLocal[3] = 'H'di C # seperti yang Anda lakukan dengan chararray C-style ; Anda harus membuat string baru sebagai gantinya. Satu-satunya cara untuk mengubahnya strLocaladalah dengan mengarahkan referensi ke string lain, dan itu berarti tidak ada yang dapat Anda lakukan untuk strLocalmemengaruhi strMain. Nilainya tidak dapat diubah, dan referensinya adalah salinan.

Melewati referensi dengan referensi

Untuk membuktikan ada perbedaan, inilah yang terjadi jika Anda memberikan referensi dengan referensi:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Kali ini, string di Mainbenar-benar berubah karena Anda meneruskan referensi tanpa menyalinnya di tumpukan.

Jadi, meskipun string adalah jenis referensi, meneruskannya dengan nilai berarti apa pun yang terjadi di callee tidak akan memengaruhi string di pemanggil. Tetapi karena mereka adalah tipe referensi, Anda tidak perlu menyalin seluruh string dalam memori saat Anda ingin menyebarkannya.

Sumber lebih lanjut:

Justin Morgan
sumber
3
@TheLight - Maaf, Anda salah di sini saat mengatakan: "Jenis referensi diberikan sebagai referensi secara default." Secara default, semua parameter diteruskan oleh nilai, tetapi dengan tipe referensi, ini berarti bahwa referensi diteruskan oleh nilai. Anda menggabungkan tipe referensi dengan parameter referensi, yang dapat dimengerti karena ini merupakan perbedaan yang sangat membingungkan. Lihat bagian Meneruskan Jenis Referensi berdasarkan Nilai di sini. Artikel tertaut Anda cukup benar, tetapi sebenarnya mendukung maksud saya.
Justin Morgan
1
@JustinMorgan Bukan untuk memunculkan utas komentar mati, tapi menurut saya komentar TheLight masuk akal jika Anda berpikir dalam C. Dalam C, data hanyalah satu blok memori. Referensi adalah penunjuk ke blok memori itu. Jika Anda melewatkan seluruh blok memori ke suatu fungsi, itu disebut "melewati nilai". Jika Anda meneruskan pointer, ini disebut "passing by reference". Dalam C #, tidak ada gagasan untuk melewatkan seluruh blok memori, jadi mereka mendefinisikan ulang "lewat nilai" yang berarti melewatkan penunjuk. Tampaknya salah, tetapi penunjuk hanyalah satu blok memori juga! Bagi saya, terminologinya cukup sewenang
rliu
@roliu - Masalahnya adalah kita tidak bekerja di C, dan C # sangat berbeda meskipun nama dan sintaksnya mirip. Untuk satu hal, referensi tidak sama dengan pointer , dan memikirkannya seperti itu dapat menyebabkan jebakan. Masalah terbesar, bagaimanapun, adalah bahwa "melewati referensi" memiliki arti yang sangat spesifik dalam C #, membutuhkan refkata kunci. Untuk membuktikan bahwa melewatkan referensi membuat perbedaan, lihat demo ini: rextester.com/WKBG5978
Justin Morgan
1
@JustinMorgan Saya setuju bahwa mencampurkan terminologi C dan C # itu buruk, tetapi, sementara saya menikmati posting lippert, saya tidak setuju bahwa memikirkan referensi sebagai petunjuk terutama mengaburkan apa pun di sini. Entri blog menjelaskan bagaimana memikirkan referensi sebagai penunjuk memberikan kekuatan yang terlalu besar. Saya menyadari bahwa refkata kunci memiliki utilitas, saya hanya mencoba menjelaskan mengapa orang mungkin berpikir untuk melewatkan jenis referensi dengan nilai di C # tampak seperti gagasan "tradisional" (yaitu C) melewati referensi (dan meneruskan jenis referensi dengan referensi di C # sepertinya lebih seperti meneruskan referensi ke referensi berdasarkan nilai).
rliu
2
Anda benar, tapi saya pikir @roliu adalah referensi bagaimana fungsi seperti Foo(string bar)bisa dianggap sebagai Foo(char* bar)sedangkan Foo(ref string bar)akan Foo(char** bar)(atau Foo(char*& bar)atau Foo(string& bar)di C ++). Tentu, ini bukan bagaimana Anda harus memikirkannya setiap hari, tetapi itu benar-benar membantu saya akhirnya memahami apa yang terjadi di balik terpal.
Cole Johnson
23

String di C # adalah objek referensi yang tidak bisa diubah. Ini berarti bahwa referensi ke sana akan diteruskan (menurut nilai), dan setelah string dibuat, Anda tidak dapat memodifikasinya. Metode yang menghasilkan versi string yang dimodifikasi (substring, versi yang dipotong, dll.) Membuat salinan yang dimodifikasi dari string asli.

dasblinkenlight
sumber
10

String adalah kasus khusus. Setiap contoh tidak dapat diubah. Saat Anda mengubah nilai string, Anda mengalokasikan string baru di memori.

Jadi hanya referensi yang diteruskan ke fungsi Anda, tetapi ketika string diedit, itu menjadi contoh baru dan tidak mengubah contoh lama.

Enigmativitas
sumber
4
String bukanlah kasus khusus dalam aspek ini. Sangat mudah untuk membuat objek yang tidak dapat diubah yang dapat memiliki semantik yang sama. (Yaitu, contoh dari tipe yang tidak mengekspos metode untuk memutasinya ...)
String adalah kasus khusus - string adalah tipe referensi yang secara efektif tidak dapat diubah yang tampaknya dapat berubah karena berperilaku seperti tipe nilai.
Enigmativitas
1
@Enigmativitas Dengan logika itu maka Uri(kelas) dan Guid(struct) juga kasus khusus. Saya tidak melihat bagaimana System.Stringtindakan seperti "tipe nilai" lebih dari tipe tetap lainnya ... dari asal kelas atau struct.
3
@pst - String memiliki semantik pembuatan khusus - tidak seperti Uri& Guid- Anda dapat menetapkan nilai literal string ke variabel string. String tersebut tampaknya bisa berubah, seperti intditugaskan kembali, tetapi itu membuat objek secara implisit - tanpa newkata kunci.
Enigmativitas
3
String adalah kasus khusus, tetapi tidak ada relevansinya dengan pertanyaan ini. Jenis nilai, jenis referensi, jenis apa pun, semuanya akan bertindak sama dalam pertanyaan ini.
Kirk Broadhurst