สตริงถูกส่งผ่านใน. NET อย่างไร


121

เมื่อฉันส่งผ่านstringไปยังฟังก์ชันตัวชี้ไปยังเนื้อหาของสตริงจะถูกส่งผ่านหรือไม่หรือสตริงทั้งหมดถูกส่งผ่านไปยังฟังก์ชันบนสแต็กอย่างที่structจะเป็น?

คำตอบ:


278

ผ่านการอ้างอิง อย่างไรก็ตามไม่ผ่านการอ้างอิงในทางเทคนิค นี่เป็นความแตกต่างที่ลึกซึ้ง แต่สำคัญมาก พิจารณารหัสต่อไปนี้:

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

มีสามสิ่งที่คุณต้องรู้เพื่อทำความเข้าใจว่าเกิดอะไรขึ้นที่นี่:

  1. สตริงคือประเภทการอ้างอิงใน C #
  2. พวกเขายังไม่เปลี่ยนรูปดังนั้นเมื่อใดก็ตามที่คุณทำบางสิ่งที่ดูเหมือนว่าคุณกำลังเปลี่ยนสตริงคุณจะไม่ทำเช่นนั้น สตริงใหม่ถูกสร้างขึ้นการอ้างอิงถูกชี้ไปที่มันและสตริงเก่าจะถูกโยนทิ้งไป
  3. แม้ว่าสตริงจะเป็นประเภทการอ้างอิง แต่strMainก็ไม่ได้ถูกส่งผ่านโดยการอ้างอิง มันเป็นประเภทอ้างอิงแต่การอ้างอิงตัวเองจะผ่านค่า ทุกครั้งที่คุณส่งพารามิเตอร์โดยไม่มีrefคีย์เวิร์ด (ไม่นับoutพารามิเตอร์) คุณจะส่งผ่านค่าบางอย่างไป

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

การส่งผ่านประเภทการอ้างอิงตามค่า: คุณกำลังทำอยู่แล้ว

C # ตัวแปรมีทั้งประเภทอ้างอิงหรือประเภทค่า C # พารามิเตอร์มีทั้งผ่านอ้างอิงหรือผ่านค่า คำศัพท์เป็นปัญหาที่นี่ สิ่งเหล่านี้ฟังดูเหมือนกัน แต่ไม่ใช่

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

นี่คือบรรทัดแรกของMainวิธีการ:

string strMain = "main";

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

DoSomething(strMain);

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

ภายใน Callee

นี่คือวิธีการด้านบนDoSomething:

void DoSomething(string strLocal)

ไม่มีrefคำหลักดังนั้นstrLocalและstrMainเป็นการอ้างอิงที่แตกต่างกันสองรายการที่ชี้ไปที่ค่าเดียวกัน ถ้าเรากำหนดใหม่strLocal...

strLocal = "local";   

... เราไม่ได้เปลี่ยนค่าที่เก็บไว้ เราใช้การอ้างอิงที่เรียกว่าstrLocalและมุ่งเป้าไปที่สตริงใหม่ล่าสุด จะเกิดอะไรขึ้นstrMainเมื่อเราทำเช่นนั้น? ไม่มีอะไร มันยังคงชี้ไปที่สายเก่า

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" 

การเปลี่ยนไม่ได้

มาเปลี่ยนสถานการณ์กันสักวินาที ลองนึกภาพว่าเราไม่ได้ทำงานกับสตริง แต่มีการอ้างอิงบางประเภทที่ไม่แน่นอนเช่นคลาสที่คุณสร้างขึ้น

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

หากคุณทำตามการอ้างอิงobjLocalถึงวัตถุที่ชี้ไปคุณสามารถเปลี่ยนคุณสมบัติได้:

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

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

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   
}

อา แต่สตริงไม่เปลี่ยนรูป! ไม่มีChangeMeคุณสมบัติให้ตั้งค่า คุณไม่สามารถทำได้strLocal[3] = 'H'ใน C # เหมือนที่คุณทำได้กับcharอาร์เรย์สไตล์ C คุณต้องสร้างสตริงใหม่ทั้งหมดแทน วิธีเดียวที่จะเปลี่ยนแปลงได้strLocalคือชี้การอ้างอิงไปที่สตริงอื่นและนั่นหมายความว่าคุณไม่strLocalสามารถทำอะไรstrMainได้ ค่าไม่เปลี่ยนรูปและการอ้างอิงเป็นสำเนา

ผ่านการอ้างอิงโดยการอ้างอิง

เพื่อพิสูจน์ว่ามีความแตกต่างนี่คือสิ่งที่จะเกิดขึ้นเมื่อคุณส่งผ่านข้อมูลอ้างอิงโดยอ้างอิง:

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

คราวนี้สตริงในMainมีการเปลี่ยนแปลงจริง ๆ เนื่องจากคุณส่งผ่านการอ้างอิงโดยไม่ได้คัดลอกลงในสแต็ก

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

แหล่งข้อมูลเพิ่มเติม:


3
@TheLight - ขออภัยคุณไม่ถูกต้องที่นี่เมื่อคุณพูดว่า: "ประเภทการอ้างอิงจะถูกส่งต่อโดยค่าเริ่มต้น" โดยค่าเริ่มต้นพารามิเตอร์ทั้งหมดจะถูกส่งผ่านด้วยค่า แต่ด้วยประเภทการอ้างอิงหมายความว่าการอ้างอิงถูกส่งผ่านด้วยค่า คุณกำลังรวมประเภทการอ้างอิงกับพารามิเตอร์อ้างอิงซึ่งเป็นที่เข้าใจได้เนื่องจากเป็นความแตกต่างที่สับสนมาก ดูส่วนการส่งผ่านประเภทการอ้างอิงตามค่าที่นี่ บทความที่เชื่อมโยงของคุณค่อนข้างถูกต้อง แต่จริงๆแล้วมันสนับสนุนประเด็นของฉัน
Justin Morgan

1
@JustinMorgan จะไม่เปิดกระทู้แสดงความคิดเห็นที่ตายแล้ว แต่ฉันคิดว่าความคิดเห็นของ TheLight เหมาะสมถ้าคุณคิดใน C ใน C ข้อมูลเป็นเพียงบล็อกของหน่วยความจำ การอ้างอิงเป็นตัวชี้ไปยังบล็อกหน่วยความจำนั้น หากคุณส่งหน่วยความจำทั้งบล็อกไปยังฟังก์ชันเรียกว่า "การส่งผ่านค่า" หากคุณผ่านตัวชี้จะเรียกว่า "ส่งโดยอ้างอิง" ใน C # ไม่มีแนวคิดเรื่องการส่งผ่านในบล็อกหน่วยความจำทั้งหมดดังนั้นพวกเขาจึงนิยาม "การส่งผ่านค่า" ใหม่เพื่อหมายถึงการส่งตัวชี้เข้าซึ่งดูเหมือนจะผิด แต่ตัวชี้ก็เป็นเพียงบล็อกหน่วยความจำเช่นกัน! สำหรับฉันคำศัพท์นั้นค่อนข้างเป็นไปตามอำเภอใจ
rliu

@roliu - ปัญหาคือเราไม่ทำงานใน C และ C # แตกต่างกันมากแม้จะมีชื่อและไวยากรณ์ที่คล้ายกัน ประการหนึ่งการอ้างอิงไม่เหมือนกับตัวชี้และการคิดแบบนั้นอาจนำไปสู่ข้อผิดพลาดได้ อย่างไรก็ตามปัญหาที่ใหญ่ที่สุดคือ "การส่งผ่านข้อมูลอ้างอิง" มีความหมายเฉพาะเจาะจงมากใน C # ซึ่งต้องใช้refคำหลัก เพื่อพิสูจน์ว่าการส่งผ่านข้อมูลอ้างอิงสร้างความแตกต่างให้ดูการสาธิตนี้: rextester.com/WKBG5978
Justin Morgan

1
@JustinMorgan ฉันยอมรับว่าการผสมคำศัพท์ C และ C # นั้นไม่ดี แต่ในขณะที่ฉันชอบโพสต์ของ lippert ฉันไม่เห็นด้วยที่การคิดว่าการอ้างอิงเป็นตัวชี้โดยเฉพาะอย่างยิ่งทำให้เกิดหมอกขึ้นที่นี่ โพสต์ในบล็อกอธิบายถึงวิธีคิดของการอ้างอิงเป็นตัวชี้ให้อำนาจมากเกินไป ฉันรู้ว่าrefคำหลักที่มียูทิลิตี้ผมก็แค่พยายามที่จะอธิบายว่าทำไมหนึ่งอาจคิดว่าการส่งผ่านชนิดการอ้างอิงโดยค่าใน C # ดูเหมือนว่า "ดั้งเดิม" (เช่น C) ความคิดของการส่งผ่านโดยการอ้างอิง (และผ่านชนิดการอ้างอิง โดยการอ้างอิงใน C # ดูเหมือนว่าจะส่งต่อการอ้างอิงไปยังการอ้างอิงตามค่า)
rliu

2
คุณถูกต้อง แต่ฉันคิดว่า @roliu กำลังอ้างถึงว่าฟังก์ชันเช่นFoo(string bar)สามารถคิดได้อย่างไรในFoo(char* bar)ขณะที่Foo(ref string bar)จะเป็นFoo(char** bar)( Foo(char*& bar)หรือFoo(string& bar)ใน C ++) แน่นอนว่าไม่ใช่วิธีที่คุณควรคิดทุกวัน แต่ในที่สุดมันก็ช่วยให้ฉันเข้าใจสิ่งที่เกิดขึ้นภายใต้ประทุน
Cole Johnson

23

สตริงใน C # เป็นวัตถุอ้างอิงที่ไม่เปลี่ยนรูป ซึ่งหมายความว่าการอ้างอิงถึงพวกเขาจะถูกส่งไปรอบ ๆ (ตามค่า) และเมื่อสร้างสตริงแล้วคุณจะไม่สามารถแก้ไขได้ วิธีการที่สร้างสตริงเวอร์ชันแก้ไข (สตริงย่อยเวอร์ชันที่ถูกตัดแต่ง ฯลฯ ) สร้างสำเนาที่แก้ไขของสตริงต้นฉบับ


10

สตริงเป็นกรณีพิเศษ แต่ละอินสแตนซ์ไม่เปลี่ยนรูป เมื่อคุณเปลี่ยนค่าของสตริงคุณกำลังจัดสรรสตริงใหม่ในหน่วยความจำ

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


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

สตริงเป็นกรณีพิเศษ - เป็นประเภทการอ้างอิงที่ไม่เปลี่ยนรูปได้อย่างมีประสิทธิภาพซึ่งดูเหมือนจะไม่แน่นอนเนื่องจากมีพฤติกรรมเหมือนประเภทค่า
Enigmativity

1
@Enigmativity โดยตรรกะนั้นแล้วUri(คลาส) และGuid (struct) ก็เป็นกรณีพิเศษเช่นกัน ฉันไม่เห็นว่าการSystem.Stringทำหน้าที่เหมือน "ประเภทค่า" เป็นอย่างไรมากกว่าประเภทอื่น ๆ ที่ไม่เปลี่ยนรูป ... ของคลาสหรือโครงสร้างต้นกำเนิด

3
@pst - สตริงมีความหมายการสร้างพิเศษ - ไม่เหมือนUri&Guid - คุณสามารถกำหนดค่าสตริง -ให้กับตัวแปรสตริงได้ สตริงดูเหมือนจะไม่แน่นอนเหมือนintถูกกำหนดใหม่ แต่เป็นการสร้างอ็อบเจกต์โดยปริยาย - ไม่มีnewคีย์เวิร์ด
Enigmativity

3
String เป็นกรณีพิเศษ แต่ไม่มีความเกี่ยวข้องกับคำถามนี้ ประเภทค่าประเภทการอ้างอิงประเภทใด ๆ ก็จะทำหน้าที่เหมือนกันในคำถามนี้
Kirk Broadhurst
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.