Các chuỗi được truyền trong .NET như thế nào?


121

Khi tôi chuyển một hàm stringcho một hàm, một con trỏ đến nội dung của chuỗi có được truyền hay không, hay toàn bộ chuỗi được chuyển cho hàm trên ngăn xếp giống như a struct?

Câu trả lời:


278

Một tham chiếu được thông qua; tuy nhiên, về mặt kỹ thuật nó không được thông qua bằng tham chiếu. Đây là một sự khác biệt tinh tế, nhưng rất quan trọng. Hãy xem xét đoạn mã sau:

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

Có ba điều bạn cần biết để hiểu điều gì xảy ra ở đây:

  1. Chuỗi là kiểu tham chiếu trong C #.
  2. Chúng cũng không thay đổi, vì vậy bất cứ khi nào bạn làm điều gì đó có vẻ như bạn đang thay đổi chuỗi, bạn không phải vậy. Một chuỗi hoàn toàn mới được tạo, tham chiếu được trỏ vào nó và chuỗi cũ bị loại bỏ.
  3. Mặc dù các chuỗi là kiểu tham chiếu, nhưng strMainkhông được chuyển bằng tham chiếu. Đó là một kiểu tham chiếu, nhưng bản thân tham chiếu được truyền theo giá trị . Bất kỳ khi nào bạn chuyển một tham số mà không có reftừ khóa (không tính outtham số), bạn đã truyền một giá trị nào đó.

Vì vậy, điều đó có nghĩa là bạn đang ... chuyển một tham chiếu theo giá trị. Vì đó là một loại tham chiếu, chỉ tham chiếu được sao chép vào ngăn xếp. Nhưng điều đó có nghĩa gì?

Chuyển các loại tham chiếu theo giá trị: Bạn đang làm điều đó

Biến C # là kiểu tham chiếu hoặc kiểu giá trị . Các tham số C # được truyền bởi tham chiếu hoặc được truyền bởi giá trị . Thuật ngữ là một vấn đề ở đây; những điều này nghe có vẻ giống nhau, nhưng không phải.

Nếu bạn chuyển một tham số BẤT KỲ loại nào và bạn không sử dụng reftừ khóa, thì bạn đã chuyển nó theo giá trị. Nếu bạn đã vượt qua nó theo giá trị, những gì bạn thực sự đã vượt qua là một bản sao. Nhưng nếu tham số là một kiểu tham chiếu, thì thứ bạn sao chép là tham chiếu, không phải bất cứ thứ gì nó đang trỏ vào.

Đây là dòng đầu tiên của Mainphương pháp:

string strMain = "main";

Chúng tôi đã tạo ra hai thứ trên dòng này: một chuỗi có giá trị mainđược lưu trữ trong bộ nhớ ở đâu đó và một biến tham chiếu được gọi là strMaintrỏ đến nó.

DoSomething(strMain);

Bây giờ chúng tôi chuyển tham chiếu đó tới DoSomething. Chúng tôi đã vượt qua nó theo giá trị, vì vậy có nghĩa là chúng tôi đã tạo một bản sao. Đó là một loại tham chiếu, vì vậy điều đó có nghĩa là chúng tôi đã sao chép tham chiếu, không phải chính chuỗi. Bây giờ chúng ta có hai tham chiếu mà mỗi tham chiếu trỏ đến cùng một giá trị trong bộ nhớ.

Bên trong callee

Đây là đầu của DoSomethingphương pháp:

void DoSomething(string strLocal)

Không có reftừ khóa, vì vậy strLocalstrMainlà hai tham chiếu khác nhau trỏ đến cùng một giá trị. Nếu chúng tôi chỉ định lại strLocal...

strLocal = "local";   

... chúng tôi đã không thay đổi giá trị được lưu trữ; chúng tôi lấy tham chiếu được gọi strLocalvà nhắm nó vào một chuỗi hoàn toàn mới. Điều gì xảy ra strMainkhi chúng ta làm điều đó? Không có gì. Nó vẫn đang chỉ vào chuỗi cũ.

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" 

Bất biến

Hãy thay đổi kịch bản trong một giây. Hãy tưởng tượng chúng ta không làm việc với các chuỗi, nhưng một số loại tham chiếu có thể thay đổi, như một lớp bạn đã tạo.

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

Nếu bạn làm theo tham chiếu objLocalđến đối tượng mà nó trỏ tới, bạn có thể thay đổi các thuộc tính của nó:

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

Vẫn chỉ có một MutableThingtrong bộ nhớ và cả tham chiếu đã sao chép và tham chiếu gốc vẫn trỏ đến nó. Các thuộc tính của MutableThingchính nó đã thay đổi :

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, nhưng các chuỗi là bất biến! Không có thuộc ChangeMetính nào để đặt. Bạn không thể làm strLocal[3] = 'H'trong C # như bạn có thể làm với charmảng kiểu C ; thay vào đó bạn phải tạo một chuỗi hoàn toàn mới. Cách duy nhất để thay đổi strLocallà trỏ tham chiếu vào một chuỗi khác và điều đó có nghĩa là không có gì bạn làm strLocalcó thể ảnh hưởng strMain. Giá trị là không thay đổi và tham chiếu là một bản sao.

Chuyển một tham chiếu bằng tham chiếu

Để chứng minh có sự khác biệt, đây là những gì sẽ xảy ra khi bạn chuyển một tham chiếu bằng tham chiếu:

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

Lần này, chuỗi trong Mainthực sự bị thay đổi bởi vì bạn đã chuyển tham chiếu mà không sao chép nó trên ngăn xếp.

Vì vậy, mặc dù các chuỗi là kiểu tham chiếu, việc chuyển chúng theo giá trị có nghĩa là bất cứ điều gì xảy ra trong callee sẽ không ảnh hưởng đến chuỗi trong trình gọi. Nhưng vì chúng kiểu tham chiếu, bạn không cần phải sao chép toàn bộ chuỗi vào bộ nhớ khi bạn muốn chuyển nó.

Các nguồn khác:


3
@TheLight - Xin lỗi, nhưng bạn không chính xác ở đây khi nói: "Loại tham chiếu được chuyển theo tham chiếu theo mặc định." Theo mặc định, tất cả các tham số được truyền theo giá trị, nhưng với các kiểu tham chiếu, điều này có nghĩa là tham chiếu được truyền theo giá trị. Bạn đang kết hợp các loại tham chiếu với các tham số tham chiếu, điều này có thể hiểu được vì đó là một sự phân biệt rất khó hiểu. Xem phần Các loại tham chiếu vượt qua theo giá trị tại đây. Bài viết được liên kết của bạn khá đúng, nhưng nó thực sự hỗ trợ quan điểm của tôi.
Justin Morgan,

1
@JustinMorgan Không đưa ra một chuỗi bình luận đã chết, nhưng tôi nghĩ bình luận của TheLight có ý nghĩa nếu bạn nghĩ trong C. Trong C, dữ liệu chỉ là một khối bộ nhớ. Tham chiếu là một con trỏ đến khối bộ nhớ đó. Nếu bạn chuyển toàn bộ khối bộ nhớ cho một hàm, đó được gọi là "truyền theo giá trị". Nếu bạn truyền con trỏ, nó được gọi là "chuyển qua tham chiếu". Trong C #, không có khái niệm truyền trong toàn bộ khối bộ nhớ, vì vậy họ đã định nghĩa lại "truyền theo giá trị" có nghĩa là truyền con trỏ vào. Điều đó có vẻ sai, nhưng một con trỏ cũng chỉ là một khối bộ nhớ! Đối với tôi, các thuật ngữ là khá tùy tiện
rliu

@roliu - Vấn đề là chúng tôi không làm việc trong C và C # cực kỳ khác biệt mặc dù tên và cú pháp tương tự. Có điều, các tham chiếu không giống như các con trỏ , và việc nghĩ về chúng theo cách đó có thể dẫn đến những cạm bẫy. Tuy nhiên, vấn đề lớn nhất là "chuyển qua tham chiếu" có một ý nghĩa rất cụ thể trong C #, yêu cầu reftừ khóa. Để chứng minh rằng việc lướt
Justin Morgan

1
@JustinMorgan Tôi đồng ý rằng việc trộn lẫn thuật ngữ C và C # là không tốt, nhưng, trong khi tôi rất thích bài đăng của lippert, tôi không đồng ý rằng suy nghĩ về các tham chiếu như con trỏ đặc biệt làm mờ bất cứ điều gì ở đây. Bài đăng trên blog mô tả cách suy nghĩ về một tham chiếu như một con trỏ cung cấp cho nó quá nhiều sức mạnh. Tôi biết rằng reftừ khóa có tiện ích, tôi chỉ cố gắng giải thích tại sao một từ khóa có thể nghĩ về việc chuyển một loại tham chiếu theo giá trị trong C # có vẻ giống như khái niệm "truyền thống" (tức là C) về việc chuyển theo tham chiếu (và chuyển một loại tham chiếu bằng cách tham chiếu trong C # có vẻ giống như việc chuyển một tham chiếu đến một tham chiếu theo giá trị).
rliu

2
Bạn đúng, nhưng tôi nghĩ @roliu đang tham khảo cách một hàm chẳng hạn như Foo(string bar)có thể được coi là Foo(char* bar)trong khi Foo(ref string bar)sẽ như thế nào Foo(char** bar)(hoặc Foo(char*& bar)hoặc Foo(string& bar)trong C ++). Chắc chắn, đó không phải là cách bạn nên nghĩ về nó hàng ngày, nhưng nó thực sự đã giúp tôi cuối cùng hiểu được những gì đang xảy ra.
Cole Johnson

23

Các chuỗi trong C # là các đối tượng tham chiếu bất biến. Điều này có nghĩa là các tham chiếu đến chúng được truyền xung quanh (theo giá trị) và khi một chuỗi được tạo, bạn không thể sửa đổi nó. Các phương thức tạo ra các phiên bản đã sửa đổi của chuỗi (chuỗi con, phiên bản bị cắt xén, v.v.) tạo ra các bản sao đã sửa đổi của chuỗi gốc.


10

Chuỗi là trường hợp đặc biệt. Mỗi trường hợp là bất biến. Khi bạn thay đổi giá trị của một chuỗi, bạn đang cấp phát một chuỗi mới trong bộ nhớ.

Vì vậy, chỉ có tham chiếu được chuyển đến hàm của bạn, nhưng khi chuỗi được chỉnh sửa, nó sẽ trở thành một phiên bản mới và không sửa đổi phiên bản cũ.


4
Chuỗi không phải là một trường hợp đặc biệt trong khía cạnh này. Rất dễ dàng để tạo các đối tượng bất biến có thể có cùng ngữ nghĩa. (Tức là, một thể hiện của một kiểu mà không tiếp xúc với một phương pháp để biến đổi nó ...)

Các chuỗi là những trường hợp đặc biệt - chúng là các kiểu tham chiếu bất biến có hiệu quả dường như có thể thay đổi được ở chỗ chúng hoạt động giống như các kiểu giá trị.
Enigmativity

1
@Enigmativity Theo logic đó thì Uri(class) và Guid(struct) cũng là các trường hợp đặc biệt. Tôi không thấy cách System.Stringhoạt động giống như một "kiểu giá trị" hơn các kiểu bất biến khác ... của nguồn gốc lớp hoặc cấu trúc.

3
@pst - Chuỗi có ngữ nghĩa tạo đặc biệt - không giống như Uri& Guid- bạn chỉ có thể gán giá trị chuỗi-chữ cho một biến chuỗi. Chuỗi dường như có thể thay đổi, giống như một chuỗi intđược gán lại, nhưng nó đang tạo một đối tượng một cách ngầm định - không có newtừ khóa.
Enigmativity

3
Chuỗi là một trường hợp đặc biệt, nhưng điều đó không liên quan đến câu hỏi này. Loại giá trị, loại tham chiếu, bất kỳ loại nào đều sẽ hoạt động giống nhau trong câu hỏi này.
Kirk Broadhurst
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.