ประเภทการอ้างอิงสตริง C #?


164

ฉันรู้ว่า "สตริง" ใน C # เป็นประเภทอ้างอิง นี่คือบน MSDN อย่างไรก็ตามรหัสนี้ใช้งานไม่ได้ตามที่ควร:

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";
    }
}

ผลลัพธ์ควรเป็น "ก่อนผ่าน" "หลังจากผ่าน" เนื่องจากฉันผ่านสตริงเป็นพารามิเตอร์และเป็นประเภทการอ้างอิงคำสั่งเอาต์พุตที่สองควรรับรู้ว่าข้อความเปลี่ยนไปในวิธี TestI อย่างไรก็ตามฉันได้รับ "ก่อนผ่าน" "ก่อนผ่าน" ทำให้ดูเหมือนว่ามีการส่งผ่านตามมูลค่าไม่ใช่โดยอ้างอิง ฉันเข้าใจว่าสายไม่เปลี่ยนรูป แต่ฉันไม่เห็นว่าจะอธิบายสิ่งที่เกิดขึ้นที่นี่ ฉันพลาดอะไรไป ขอบคุณ


ดูบทความที่อ้างอิงโดยจอนด้านล่าง พฤติกรรมที่คุณพูดถึงสามารถทำซ้ำได้ด้วยพอยน์เตอร์ C ++ เช่นกัน
Sesh

คำอธิบายที่ดีมากในMSDNเช่นกัน
Dimi_Pel

คำตอบ:


211

การอ้างอิงไปยังสตริงถูกส่งผ่านตามค่า มีความแตกต่างใหญ่ระหว่างการส่งการอ้างอิงตามค่าและการส่งผ่านวัตถุโดยการอ้างอิง โชคไม่ดีที่มีการใช้คำว่า "การอ้างอิง" ในทั้งสองกรณี

ถ้าคุณทำผ่านการอ้างอิงสตริงโดยอ้างอิงก็จะทำงานตามที่คุณคาดหวัง:

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";
    }
}

ตอนนี้คุณต้องแยกความแตกต่างระหว่างการเปลี่ยนแปลงวัตถุที่การอ้างอิงอ้างถึงและทำการเปลี่ยนแปลงตัวแปร (เช่นพารามิเตอร์) เพื่อให้มันอ้างถึงวัตถุอื่น เราไม่สามารถเปลี่ยนแปลงสตริงได้เนื่องจากสตริงไม่เปลี่ยนรูป แต่เราสามารถแสดงให้เห็นได้ด้วยStringBuilder:

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");
    }
}

ดูบทความของฉันเกี่ยวกับการผ่านพารามิเตอร์เพื่อดูรายละเอียดเพิ่มเติม


2
เห็นด้วยเพียงแค่ต้องการทำให้ชัดเจนว่าการใช้ตัวดัดแปลงการอ้างอิงยังทำงานได้กับประเภทที่ไม่อ้างอิงเช่นทั้งสองเป็นแนวคิดที่แยกจากกัน
eglasius

2
@ Jon Skeet ชอบ sidenote ในบทความของคุณ คุณควรreferencedเป็นเช่นนั้นในฐานะที่เป็นคำตอบของคุณ
Nithish Inpursuit Ofhappiness

36

ถ้าเราต้องตอบคำถาม: สตริงเป็นประเภทการอ้างอิงและมันจะทำหน้าที่เป็นข้อมูลอ้างอิง เราส่งพารามิเตอร์ที่เก็บการอ้างอิงไม่ใช่สตริงจริง ปัญหาอยู่ในฟังก์ชั่น:

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

พารามิเตอร์testมีการอ้างอิงไปยังสตริง แต่เป็นสำเนา เรามีสองตัวแปรชี้ไปที่สตริง และเนื่องจากการดำเนินการใด ๆ กับสตริงสร้างวัตถุใหม่จริง ๆ เราจึงทำสำเนาโลคัลของเราให้ชี้ไปที่สตริงใหม่ แต่testตัวแปรดั้งเดิมจะไม่เปลี่ยนแปลง

วิธีแก้ปัญหาที่แนะนำให้ใส่refในการประกาศฟังก์ชั่นและในการเรียกใช้งานเพราะเราจะไม่ผ่านค่าของtestตัวแปร แต่จะผ่านเพียงการอ้างอิงถึงมัน ดังนั้นการเปลี่ยนแปลงใด ๆ ภายในฟังก์ชั่นจะสะท้อนถึงตัวแปรดั้งเดิม

ฉันต้องการที่จะทำซ้ำในตอนท้าย: สตริงเป็นประเภทอ้างอิง แต่เนื่องจากไม่เปลี่ยนรูปของเส้นtest = "after passing";จริงสร้างวัตถุใหม่และสำเนาของตัวแปรของเราtestจะเปลี่ยนไปชี้ไปที่สตริงใหม่


25

ดังที่คนอื่น ๆ ได้กล่าวไว้Stringประเภทใน. NET จะไม่เปลี่ยนรูปและการอ้างอิงของมันจะถูกส่งผ่านโดยค่า

ในรหัสต้นฉบับทันทีที่บรรทัดนี้ทำงาน:

test = "after passing";

จากนั้นtestจะไม่มีการอ้างถึงวัตถุต้นฉบับอีกต่อไป เราได้สร้างวัตถุใหม่ Stringและมอบหมายtestให้อ้างอิงวัตถุนั้นบนฮีปที่มีการจัดการ

ฉันรู้สึกว่ามีหลายคนสะดุดที่นี่เนื่องจากไม่มีคอนสตรัคชันที่มองเห็นได้เพื่อเตือนพวกเขา ในกรณีนี้มันเกิดขึ้นหลังฉากเนื่องจากStringประเภทมีภาษารองรับในการสร้าง

ดังนั้นนี่คือสาเหตุที่การเปลี่ยนแปลงtestไม่สามารถมองเห็นได้นอกขอบเขตของTestI(string)วิธีการ - เราได้ผ่านการอ้างอิงตามค่าและตอนนี้ค่านั้นเปลี่ยนไป! แต่ถ้าการStringอ้างอิงถูกส่งผ่านโดยการอ้างอิงดังนั้นเมื่อการอ้างอิงเปลี่ยนไปเราจะเห็นมันนอกขอบเขตของTestI(string)วิธีการ

ทั้งเตะหรือออกคำหลักที่มีความจำเป็นในกรณีนี้ ฉันรู้สึกว่าoutคำหลักอาจเหมาะกว่าสำหรับสถานการณ์นี้เล็กน้อย

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 = เริ่มต้นฟังก์ชั่นภายนอก, out = เริ่มต้นภายในฟังก์ชั่นหรือในคำอื่น ๆ ; ref เป็นสองทาง, out out-only ดังนั้นควรอ้างอิงอย่างแน่นอน
Paul Zahra

@PaulZahra: outจำเป็นต้องได้รับมอบหมายภายในวิธีการสำหรับการรวบรวมรหัส refไม่มีข้อกำหนดดังกล่าว นอกจากนี้outพารามิเตอร์จะถูกเตรียมใช้งานนอกเมธอด - โค้ดในคำตอบนี้คือตัวอย่าง
Derek W

ควรชี้แจง - outพารามิเตอร์สามารถเริ่มต้นได้นอกวิธีการ แต่ไม่จำเป็นต้อง ในกรณีนี้เราต้องการเริ่มต้นoutพารามิเตอร์เพื่อแสดงจุดเกี่ยวกับลักษณะของstringประเภทใน. NET
Derek W

9

จริงๆแล้วมันจะเหมือนกันสำหรับวัตถุใด ๆ สำหรับเรื่องนั้น ๆ เช่นการเป็นประเภทการอ้างอิงและการส่งผ่านการอ้างอิงนั้นมี 2 สิ่งที่แตกต่างกันใน c #

สิ่งนี้จะใช้งานได้ แต่ใช้ได้กับทุกประเภท

public static void TestI(ref string test)

ยังเกี่ยวกับสตริงที่เป็นประเภทอ้างอิงอีกทั้งยังเป็นสตริงพิเศษ มันออกแบบมาให้ไม่เปลี่ยนรูปดังนั้นวิธีการทั้งหมดจะไม่แก้ไขอินสแตนซ์ (พวกมันคืนค่าใหม่) นอกจากนี้ยังมีสิ่งพิเศษในการแสดง


7

ต่อไปนี้เป็นวิธีที่ดีในการคิดเกี่ยวกับความแตกต่างระหว่างประเภทค่า, การผ่านตามตัวอักษร, ประเภทการอ้างอิงและการผ่านตามการอ้างอิง:

ตัวแปรคือคอนเทนเนอร์

ตัวแปรชนิดค่ามีอินสแตนซ์ ตัวแปรชนิดการอ้างอิงประกอบด้วยตัวชี้ไปยังอินสแตนซ์ที่เก็บไว้ที่อื่น

การแก้ไขตัวแปรชนิดค่าเปลี่ยนแปลงอินสแตนซ์ที่มีอยู่ การแก้ไขตัวแปรชนิดการอ้างอิงจะเปลี่ยนแปลงอินสแตนซ์ที่ชี้ไป

ตัวแปรชนิดการอ้างอิงแยกสามารถชี้ไปที่อินสแตนซ์เดียวกันได้ ดังนั้นอินสแตนซ์เดียวกันสามารถกลายพันธุ์ผ่านตัวแปรใด ๆ ที่ชี้ไปที่มัน

อาร์กิวเมนต์ที่ส่งผ่านค่าเป็นคอนเทนเนอร์ใหม่พร้อมสำเนาใหม่ของเนื้อหา อาร์กิวเมนต์แบบส่งต่อโดยอ้างอิงเป็นคอนเทนเนอร์ดั้งเดิมที่มีเนื้อหาดั้งเดิม

เมื่ออาร์กิวเมนต์ชนิดค่าถูกส่งผ่านตามค่า: การกำหนดเนื้อหาของอาร์กิวเมนต์ใหม่จะไม่มีผลใด ๆ นอกขอบเขตเนื่องจากคอนเทนเนอร์นั้นไม่ซ้ำกัน การแก้ไขอาร์กิวเมนต์ไม่มีผลนอกขอบเขตเนื่องจากอินสแตนซ์เป็นสำเนาอิสระ

เมื่ออาร์กิวเมนต์ชนิดอ้างอิงถูกส่งผ่านตามค่า: การกำหนดเนื้อหาของอาร์กิวเมนต์ใหม่จะไม่มีผลใด ๆ นอกขอบเขตเนื่องจากคอนเทนเนอร์นั้นไม่ซ้ำกัน การแก้ไขเนื้อหาของอาร์กิวเมนต์ส่งผลต่อขอบเขตภายนอกเนื่องจากตัวชี้ที่คัดลอกชี้ไปยังอินสแตนซ์ที่แชร์

เมื่ออาร์กิวเมนต์ใด ๆ ถูกส่งผ่านโดยอ้างอิง: การกำหนดเนื้อหาของอาร์กิวเมนต์ใหม่จะมีผลกับขอบเขตภายนอกเนื่องจากมีการแชร์คอนเทนเนอร์ การแก้ไขเนื้อหาของอาร์กิวเมนต์ส่งผลต่อขอบเขตภายนอกเนื่องจากเนื้อหาถูกแชร์

สรุปแล้ว:

ตัวแปรสตริงเป็นตัวแปรชนิดอ้างอิง ดังนั้นมันจึงมีตัวชี้ไปยังอินสแตนซ์ที่เก็บไว้ที่อื่น เมื่อส่งผ่านค่าตัวชี้จะถูกคัดลอกดังนั้นการแก้ไขอาร์กิวเมนต์สตริงควรส่งผลกระทบต่ออินสแตนซ์ที่แชร์ อย่างไรก็ตามอินสแตนซ์สตริงไม่มีคุณสมบัติที่ไม่แน่นอนดังนั้นอาร์กิวเมนต์สตริงจึงไม่สามารถแก้ไขได้ เมื่อผ่านการอ้างอิงคอนเทนเนอร์ของตัวชี้จะถูกแชร์ดังนั้นการกำหนดใหม่จะยังคงส่งผลต่อขอบเขตภายนอก


6

" ภาพที่มีค่าพันคำ "

ฉันมีตัวอย่างง่ายๆที่นี่มันคล้ายกับกรณีของคุณ

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

นี่คือสิ่งที่เกิดขึ้น:

ป้อนคำอธิบายรูปภาพที่นี่

  • บรรทัดที่ 1 และ 2: s1และs2ตัวแปรอ้างอิงถึง"abc"วัตถุสตริงเดียวกัน
  • บรรทัดที่ 3: เนื่องจากสตริงไม่สามารถเปลี่ยนแปลงได้ดังนั้น"abc"วัตถุสตริงจะไม่แก้ไขตัวเอง (เป็น"def") แต่จะมีการสร้าง"def"วัตถุสตริงใหม่แทนและs1อ้างอิงกับมัน
  • บรรทัดที่ 4: s2ยังอ้างอิงถึง"abc"วัตถุสตริงดังนั้นนั่นคือผลลัพธ์

5

คำตอบข้างต้นมีประโยชน์ฉันต้องการเพิ่มตัวอย่างที่ฉันคิดว่าแสดงให้เห็นชัดเจนว่าเกิดอะไรขึ้นเมื่อเราส่งพารามิเตอร์โดยไม่มีคำหลักอ้างอิงแม้ว่าพารามิเตอร์นั้นเป็นประเภทการอ้างอิง:

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
คำอธิบายนี้ทำงานได้ดีที่สุดสำหรับฉัน ดังนั้นโดยทั่วไปเราผ่านทุกอย่างตามตัวอักษรแม้ว่าตัวแปรนั้นจะเป็นค่าหรือประเภทการอ้างอิงเว้นแต่ว่าเราใช้คำหลักอ้างอิง (หรือออก) มันไม่ได้โดดเด่นด้วยการเขียนโปรแกรมแบบวันต่อวันเพราะเราไม่ได้ตั้งวัตถุของเราเป็นโมฆะหรือเป็นตัวอย่างที่แตกต่างกันภายในวิธีการที่พวกเขาผ่านเราค่อนข้างจะกำหนดคุณสมบัติหรือเรียกวิธีการของพวกเขา ในกรณีของ "string" การตั้งค่าให้กับอินสแตนซ์ใหม่นั้นเกิดขึ้นตลอดเวลา แต่การมองขึ้นใหม่นั้นไม่สามารถมองเห็นได้ ถูกต้องฉันถ้าผิด
ΕГИІИО

3

สำหรับจิตใจที่อยากรู้อยากเห็นและทำให้การสนทนาเสร็จสมบูรณ์: ใช่ String เป็นประเภทข้อมูลอ้างอิง :

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

แต่โปรดทราบว่าการเปลี่ยนแปลงนี้ใช้ได้กับบล็อกที่ไม่ปลอดภัยเท่านั้น! เนื่องจากสตริงไม่เปลี่ยนรูป (จาก MSDN):

เนื้อหาของวัตถุสตริงไม่สามารถเปลี่ยนแปลงได้หลังจากที่วัตถุถูกสร้างขึ้นแม้ว่าไวยากรณ์จะทำให้มันปรากฏราวกับว่าคุณสามารถทำได้ ตัวอย่างเช่นเมื่อคุณเขียนรหัสนี้คอมไพเลอร์จริงสร้างวัตถุสตริงใหม่เพื่อเก็บลำดับของตัวละครใหม่และวัตถุใหม่ที่ได้รับมอบหมายให้ b สตริง "h" จะมีสิทธิ์ใช้งานการรวบรวมขยะ

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

และโปรดทราบว่า:

แม้ว่าสตริงเป็นชนิดการอ้างอิงตัวดำเนินการความเท่าเทียมกัน ( ==และ !=) ถูกกำหนดเพื่อเปรียบเทียบค่าของวัตถุสตริงไม่ใช่การอ้างอิง


0

ฉันเชื่อว่ารหัสของคุณคล้ายคลึงกับรายการต่อไปนี้และคุณไม่ควรคาดหวังว่ามูลค่าจะเปลี่ยนแปลงด้วยเหตุผลเดียวกับที่มันไม่ได้เกิดขึ้นที่นี่:

 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

ลอง:


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

3
คำตอบของคุณควรมีมากกว่ารหัส ควรมีคำอธิบายว่าทำไมมันถึงทำงาน
Charles Caldwell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.