Kiểu tham chiếu chuỗi C #?


163

Tôi biết rằng "chuỗi" trong C # là một loại tham chiếu. Đây là trên MSDN. Tuy nhiên, mã này không hoạt động như sau:

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

Đầu ra phải là "trước khi truyền" "sau khi truyền" vì tôi chuyển chuỗi dưới dạng tham số và nó là kiểu tham chiếu, câu lệnh đầu ra thứ hai sẽ nhận ra rằng văn bản đã thay đổi trong phương thức TestI. Tuy nhiên, tôi nhận được "trước khi vượt qua" "trước khi vượt qua" làm cho có vẻ như nó được thông qua bởi giá trị không phải bởi ref. Tôi hiểu rằng các chuỗi là bất biến, nhưng tôi không thấy điều đó sẽ giải thích những gì đang diễn ra ở đây. Tôi đang thiếu gì? Cảm ơn.


Xem bài viết được giới thiệu bởi Jon dưới đây. Hành vi bạn đề cập cũng có thể được sao chép bằng con trỏ C ++.
Sesh

Giải thích rất hay trong MSDN cũng có.
Dimi_Pel

Câu trả lời:


211

Tham chiếu đến chuỗi được truyền theo giá trị. Có một sự khác biệt lớn giữa việc chuyển một tham chiếu theo giá trị và truyền một đối tượng bằng tham chiếu. Thật không may là từ "tham chiếu" được sử dụng trong cả hai trường hợp.

Nếu bạn làm vượt qua tham chiếu chuỗi bằng cách tham khảo, nó sẽ làm việc như bạn mong đợi:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

Bây giờ bạn cần phân biệt giữa việc thực hiện các thay đổi đối với đối tượng mà tham chiếu đề cập đến và thực hiện thay đổi đối với một biến (chẳng hạn như tham số) để cho phép tham chiếu đến một đối tượng khác. Chúng tôi không thể thay đổi chuỗi vì chuỗi không thay đổi, nhưng StringBuilderthay vào đó chúng tôi có thể chứng minh chuỗi đó:

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

Xem bài viết của tôi về thông số đi qua để biết thêm chi tiết.


2
đồng ý, chỉ muốn làm rõ rằng sử dụng công cụ sửa đổi ref cũng hoạt động cho các loại không tham chiếu tức là cả hai đều là các khái niệm khá riêng biệt.
eglasius

2
@Jon Skeet yêu thích sidenote trong bài viết của bạn. Bạn nên referencedcó câu trả lời của mình
Nithish Inpursuit Ofhappiness 4/12/12

36

Nếu chúng ta phải trả lời câu hỏi: Chuỗi là một loại tham chiếu và nó hoạt động như một tham chiếu. Chúng tôi chuyển một tham số chứa tham chiếu đến, không phải chuỗi thực tế. Vấn đề là ở chức năng:

public static void TestI(string test)
{
    test = "after passing";
}

Tham số testgiữ tham chiếu đến chuỗi nhưng nó là bản sao. Chúng tôi có hai biến chỉ vào chuỗi. Và bởi vì bất kỳ hoạt động nào với chuỗi thực sự tạo ra một đối tượng mới, chúng tôi tạo bản sao cục bộ của chúng tôi để trỏ đến chuỗi mới. Nhưng testbiến ban đầu không thay đổi.

Các giải pháp được đề xuất để đưa refvào khai báo hàm và trong công việc gọi vì chúng ta sẽ không chuyển giá trị của testbiến mà sẽ chỉ chuyển một tham chiếu đến nó. Do đó, bất kỳ thay đổi bên trong hàm sẽ phản ánh biến ban đầu.

Tôi muốn lặp lại ở cuối: Chuỗi là một kiểu tham chiếu nhưng vì nó bất biến nên dòng test = "after passing";thực sự tạo ra một đối tượng mới và bản sao biến của chúng tôi testđược thay đổi để trỏ đến chuỗi mới.


25

Như những người khác đã nói, Stringloại trong .NET là bất biến và tham chiếu của nó được truyền theo giá trị.

Trong mã gốc, ngay khi dòng này thực thi:

test = "after passing";

sau đó testkhông còn đề cập đến đối tượng ban đầu. Chúng tôi đã tạo một đối tượng mới String và được chỉ định testđể tham chiếu đối tượng đó trên heap được quản lý.

Tôi cảm thấy rằng nhiều người bị vấp ngã ở đây vì không có nhà xây dựng chính thức có thể nhìn thấy để nhắc nhở họ. Trong trường hợp này, nó xảy ra đằng sau hậu trường vì Stringloại này có hỗ trợ ngôn ngữ trong cách nó được xây dựng.

Do đó, đây là lý do tại sao thay đổi testkhông hiển thị ngoài phạm vi của TestI(string)phương thức - chúng tôi đã chuyển tham chiếu theo giá trị và bây giờ giá trị đó đã thay đổi! Nhưng nếu Stringtham chiếu được truyền bằng tham chiếu, thì khi tham chiếu thay đổi, chúng ta sẽ thấy nó nằm ngoài phạm vi của TestI(string)phương thức.

Hoặc là ref hay ra từ khóa là cần thiết trong trường hợp này. Tôi cảm thấy outtừ khóa có thể phù hợp hơn một chút cho tình huống cụ thể này.

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}

ref = khởi tạo chức năng bên ngoài, out = khởi tạo bên trong chức năng, hay nói cách khác; ref là hai cách, out là out-only. Vì vậy, chắc chắn ref nên được sử dụng.
Paul Zahra

@PaulZahra: outcần được chỉ định trong phương thức để mã biên dịch. refkhông có yêu cầu như vậy. Ngoài ra outcác tham số được khởi tạo bên ngoài phương thức - mã trong câu trả lời này là một ví dụ mẫu.
Derek W

Nên làm rõ - outcác tham số có thể được khởi tạo bên ngoài phương thức, nhưng không phải. Trong trường hợp này, chúng tôi muốn khởi tạo outtham số để chứng minh một điểm về bản chất của stringloại trong .NET.
Derek W

9

Trên thực tế, nó sẽ giống nhau cho bất kỳ đối tượng nào cho vấn đề đó, tức là một kiểu tham chiếu và chuyển qua tham chiếu là 2 điều khác nhau trong c #.

Điều này sẽ làm việc, nhưng áp dụng bất kể loại nào:

public static void TestI(ref string test)

Ngoài ra về chuỗi là một loại tham chiếu, nó cũng là một loại đặc biệt. Nó được thiết kế để không thay đổi, vì vậy tất cả các phương thức của nó sẽ không sửa đổi thể hiện (chúng trả về một thể hiện mới). Nó cũng có một số điều thêm vào cho hiệu suất.


7

Đây là một cách hay để suy nghĩ về sự khác biệt giữa các loại giá trị, chuyển qua giá trị, loại tham chiếu và chuyển qua tham chiếu:

Một biến là một container.

Một biến kiểu giá trị chứa một thể hiện. Một biến kiểu tham chiếu chứa một con trỏ tới một thể hiện được lưu trữ ở nơi khác.

Sửa đổi một biến loại giá trị làm thay đổi thể hiện mà nó chứa. Sửa đổi một biến kiểu tham chiếu làm thay đổi thể hiện mà nó trỏ tới.

Các biến kiểu tham chiếu riêng biệt có thể trỏ đến cùng một thể hiện. Do đó, cùng một thể hiện có thể được thay đổi thông qua bất kỳ biến nào trỏ đến nó.

Đối số được truyền qua giá trị là một vùng chứa mới với một bản sao mới của nội dung. Đối số được thông qua tham chiếu là vùng chứa ban đầu với nội dung ban đầu của nó.

Khi đối số loại giá trị được truyền qua giá trị: Việc gán lại nội dung của đối số không có tác dụng ngoài phạm vi, bởi vì vùng chứa là duy nhất. Sửa đổi đối số không có tác dụng ngoài phạm vi, vì thể hiện là một bản sao độc lập.

Khi một đối số kiểu tham chiếu được truyền theo giá trị: Việc gán lại nội dung của đối số không có tác dụng ngoài phạm vi, bởi vì vùng chứa là duy nhất. Sửa đổi nội dung của đối số ảnh hưởng đến phạm vi bên ngoài, vì con trỏ được sao chép trỏ đến một thể hiện được chia sẻ.

Khi bất kỳ đối số nào được truyền qua tham chiếu: Việc gán lại nội dung của đối số sẽ ảnh hưởng đến phạm vi bên ngoài, vì vùng chứa được chia sẻ. Sửa đổi nội dung của đối số ảnh hưởng đến phạm vi bên ngoài, vì nội dung được chia sẻ.

Tóm lại là:

Một biến chuỗi là một biến kiểu tham chiếu. Do đó, nó chứa một con trỏ tới một thể hiện được lưu trữ ở nơi khác. Khi truyền qua giá trị, con trỏ của nó được sao chép, do đó sửa đổi đối số chuỗi sẽ ảnh hưởng đến thể hiện được chia sẻ. Tuy nhiên, một thể hiện chuỗi không có thuộc tính có thể thay đổi, do đó, một đối số chuỗi không thể được sửa đổi. Khi được truyền qua tham chiếu, vùng chứa của con trỏ được chia sẻ, do đó việc gán lại vẫn sẽ ảnh hưởng đến phạm vi bên ngoài.


6

" Một bức tranh đáng giá ngàn lời nói ".

Tôi có một ví dụ đơn giản ở đây, nó tương tự như trường hợp của bạn.

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

Đây là những gì đã xảy ra:

nhập mô tả hình ảnh ở đây

  • Dòng 1 và 2: s1s2các biến tham chiếu đến cùng một "abc"đối tượng chuỗi.
  • Dòng 3: Vì các chuỗi là bất biến , nên "abc"đối tượng chuỗi không tự sửa đổi (thành "def"), nhưng một "def"đối tượng chuỗi mới được tạo thay thế, và sau đó s1tham chiếu đến nó.
  • Dòng 4: s2vẫn tham chiếu đến "abc"đối tượng chuỗi, vì vậy đó là đầu ra.

5

Các câu trả lời ở trên rất hữu ích, tôi chỉ muốn thêm một ví dụ mà tôi nghĩ là đang chứng minh rõ ràng điều gì xảy ra khi chúng ta truyền tham số mà không có từ khóa ref, ngay cả khi tham số đó là loại tham chiếu:

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }

1
Giải thích này làm việc cho tôi tốt nhất. Vì vậy, về cơ bản, chúng tôi vượt qua mọi thứ theo giá trị mặc dù thực tế rằng chính biến đó là giá trị hoặc loại tham chiếu trừ khi chúng tôi sử dụng từ khóa ref (hoặc ngoài). Nó không nổi bật với mã hóa hàng ngày của chúng ta bởi vì chúng ta thường không đặt các đối tượng của mình thành null hoặc thành một thể hiện khác trong một phương thức mà chúng được truyền vào, thay vào đó chúng ta đặt các thuộc tính của chúng hoặc gọi các phương thức của chúng. Trong trường hợp của "chuỗi", việc đặt nó thành một thể hiện mới luôn luôn xảy ra nhưng việc làm mới không hiển thị và điều đó mang lại một sự giải thích sai cho mắt không được huấn luyện. Sửa tôi nếu sai.
Ε é И

3

Đối với những người tò mò và hoàn thành cuộc trò chuyện: Có, Chuỗi là loại tham chiếu :

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

Nhưng lưu ý rằng thay đổi này chỉ hoạt động trong một khối không an toàn ! bởi vì Chuỗi là bất biến (Từ MSDN):

Nội dung của một đối tượng chuỗi không thể thay đổi sau khi đối tượng được tạo, mặc dù cú pháp làm cho nó xuất hiện như thể bạn có thể làm điều này. Ví dụ, khi bạn viết mã này, trình biên dịch thực sự tạo ra một đối tượng chuỗi mới để giữ chuỗi ký tự mới và đối tượng mới đó được gán cho b. Chuỗi "h" sau đó đủ điều kiện để thu gom rác.

string b = "h";  
b += "ello";  

Và hãy nhớ rằng:

Mặc dù chuỗi là một kiểu tham chiếu, các toán tử đẳng thức ( ==!=) được xác định để so sánh các giá trị của các đối tượng chuỗi, không phải tham chiếu.


0

Tôi tin rằng mã của bạn tương tự như sau và bạn không nên mong đợi giá trị đã thay đổi vì lý do tương tự như vậy ở đây:

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }

-1

Thử:


public static void TestI(ref string test)
    {
        test = "after passing";
    }

3
Câu trả lời của bạn không chỉ chứa mã. Nó cũng nên chứa một lời giải thích về lý do tại sao nó hoạt động.
Charles Caldwell
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.