เหตุใดจึงต้องใช้คำหลัก 'อ้างอิง' เมื่อส่งผ่านวัตถุ


290

หากฉันส่งวัตถุไปยังวิธีการเหตุใดฉันจึงควรใช้คำหลักอ้างอิง นี่ไม่ใช่พฤติกรรมเริ่มต้นใช่หรือไม่

ตัวอย่างเช่น:

class Program
{
    static void Main(string[] args)
    {
        TestRef t = new TestRef();
        t.Something = "Foo";

        DoSomething(t);
        Console.WriteLine(t.Something);
    }

    static public void DoSomething(TestRef t)
    {
        t.Something = "Bar";
    }
}


public class TestRef
{
    public string Something { get; set; }
}

ผลลัพธ์คือ "บาร์" ซึ่งหมายความว่าวัตถุถูกส่งผ่านเป็นการอ้างอิง

คำตอบ:


299

ผ่าน a refหากคุณต้องการเปลี่ยนสิ่งที่เป็นวัตถุ:

TestRef t = new TestRef();
t.Something = "Foo";
DoSomething(ref t);

void DoSomething(ref TestRef t)
{
  t = new TestRef();
  t.Something = "Not just a changed t, but a completely different TestRef object";
}

หลังจากเรียก DoSomething tไม่ได้อ้างถึงต้นฉบับnew TestRefแต่หมายถึงวัตถุที่แตกต่างอย่างสิ้นเชิง

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

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

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

int x = 1;
Change(ref x);
Debug.Assert(x == 5);
WillNotChange(x);
Debug.Assert(x == 5); // Note: x doesn't become 10

void Change(ref int x)
{
  x = 5;
}

void WillNotChange(int x)
{
  x = 10;
}

88

คุณต้องแยกความแตกต่างระหว่าง "ผ่านการอ้างอิงโดยค่า" และ "ผ่านพารามิเตอร์ / อาร์กิวเมนต์โดยการอ้างอิง"

ฉันได้เขียนบทความยาว ๆ พอสมควรในเรื่องเพื่อหลีกเลี่ยงการเขียนอย่างระมัดระวังในแต่ละครั้งที่เกิดขึ้นในกลุ่มข่าว :)


1
ฉันพบปัญหาขณะอัพเกรด VB6 เป็น. Net C # code มีลายเซ็นของฟังก์ชัน / เมธอดที่รับพารามิเตอร์ ref, out และ plain แล้วเราจะแยกแยะความแตกต่างระหว่าง param ธรรมดากับ ref ได้ดีขึ้นอย่างไร
bonCodigo

2
@bonCodigo: ไม่แน่ใจว่าคุณหมายถึง "better แยกความแตกต่าง" - มันเป็นส่วนหนึ่งของลายเซ็นและคุณต้องระบุrefที่ไซต์การโทรด้วย ... คุณต้องการให้มันโดดเด่นที่ไหนอีก ความหมายนั้นมีเหตุผลที่ชัดเจนเช่นกัน แต่ต้องแสดงอย่างระมัดระวัง (แทนที่จะเป็น "วัตถุที่ถูกส่งผ่านโดยการอ้างอิง" ซึ่งเป็นเรื่องธรรมดาที่ทำให้เข้าใจง่าย)
Jon Skeet

ฉันไม่รู้ว่าทำไม Visual Studio ยังไม่แสดงสิ่งที่ผ่านไปอย่างชัดเจน
MonsterMMORPG

3
@ MonsterMMORPG: ฉันไม่รู้ว่าคุณหมายถึงอะไรฉันกลัว
Jon Skeet

57

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

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

ดังที่ผู้คนเคยกล่าวไว้ก่อนหน้านี้ว่าการมอบหมายคือการแก้ไขการอ้างอิงดังนั้นการสูญเสีย:

public void Method1(object obj) {   
 obj = new Object(); 
}

public void Method2(object obj) {  
 obj = _privateObject; 
}

วิธีการข้างต้นไม่ได้ปรับเปลี่ยนวัตถุต้นฉบับ

การดัดแปลงตัวอย่างของคุณเล็กน้อย

 using System;

    class Program
        {
            static void Main(string[] args)
            {
                TestRef t = new TestRef();
                t.Something = "Foo";

                DoSomething(t);
                Console.WriteLine(t.Something);

            }

            static public void DoSomething(TestRef t)
            {
                t = new TestRef();
                t.Something = "Bar";
            }
        }



    public class TestRef
    {
    private string s;
        public string Something 
        { 
            get {return s;} 
            set { s = value; }
        }
    }

6
ฉันชอบคำตอบนี้ดีกว่าแล้วคำตอบที่ยอมรับ มันอธิบายได้ชัดเจนยิ่งขึ้นว่าเกิดอะไรขึ้นเมื่อส่งตัวแปรประเภทการอ้างอิงด้วยคำหลักอ้างอิง ขอบคุณ!
สเตฟาน

17

เนื่องจาก TestRef เป็นคลาส (ซึ่งเป็นวัตถุอ้างอิง) คุณสามารถเปลี่ยนเนื้อหาภายใน t โดยไม่ต้องผ่านมันเป็นอ้างอิง อย่างไรก็ตามถ้าคุณผ่านการอ้างอิง t, TestRef สามารถเปลี่ยนสิ่งที่ต้นฉบับ t หมายถึง เช่นทำให้มันชี้ไปที่วัตถุที่แตกต่าง


16

ด้วยrefคุณสามารถเขียน:

static public void DoSomething(ref TestRef t)
{
    t = new TestRef();
}

และ t จะถูกเปลี่ยนหลังจากวิธีการเสร็จสมบูรณ์


8

คิดว่าตัวแปร (เช่นfoo) ประเภทอ้างอิง (เช่นList<T>) เป็นตัวระบุวัตถุของรูปแบบ "Object # 24601" สมมติว่าคำสั่งfoo = new List<int> {1,5,7,9};ทำให้เกิดfooการถือ "Object # 24601" (รายการที่มีสี่รายการ) จากนั้นการโทรfoo.Lengthจะถามความยาวของ Object # 24601 และมันจะตอบกลับ 4 ดังนั้นfoo.Lengthจะเท่ากับ 4

หากfooถูกส่งไปยังวิธีการหนึ่งโดยไม่ใช้refวิธีนั้นอาจทำการเปลี่ยนแปลง Object # 24601 เป็นผลมาจากการเปลี่ยนแปลงดังกล่าวfoo.Lengthอาจไม่เท่ากับ 4 อีกต่อไปอย่างไรก็ตามวิธีการเองจะไม่สามารถเปลี่ยนแปลงfooได้ซึ่งจะยังคง "Object # 24601" ต่อไป

การส่งfooเป็นrefพารามิเตอร์จะอนุญาตให้เมธอดที่เรียกใช้ทำการเปลี่ยนแปลงไม่เพียง แต่กับ Object # 24601 แต่ยังรวมถึงfooตัวมันเองด้วย วิธีการที่จะสร้างวัตถุใหม่ # 8675309 fooและจัดเก็บการอ้างอิงถึงว่าใน หากทำเช่นนั้นfooจะไม่ระงับ "Object # 24601" อีกต่อไป แต่จะแทนที่ "Object # 8675309"

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


5

นี่เหมือนกับการส่งตัวชี้ไปยังตัวชี้ใน C ใน. NET นี้จะช่วยให้คุณเปลี่ยนสิ่งที่ T ดั้งเดิมหมายถึงโดยส่วนตัวแม้ว่าฉันคิดว่าถ้าคุณทำเช่นนั้นใน. NET คุณอาจมีปัญหาการออกแบบ!


3

โดยการใช้refคำหลักที่มีประเภทการอ้างอิงคุณจะผ่านการอ้างอิงไปยังการอ้างอิงได้อย่างมีประสิทธิภาพ ในหลาย ๆ วิธีมันเหมือนกับการใช้outคำหลัก แต่มีความแตกต่างเล็กน้อยที่ไม่มีการรับประกันว่าวิธีการนั้นจะกำหนดอะไรให้กับrefพารามิเตอร์ 'ed


3

ref เลียนแบบ (หรือพฤติกรรม) เป็นพื้นที่ส่วนกลางเพียงสองขอบเขต:

  • ผู้เรียก
  • callee

1

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


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

1

Ref หมายถึงว่าฟังก์ชั่นสามารถจับมือกับวัตถุเองหรือตามมูลค่าของมันเท่านั้น

การส่งต่อโดยการอ้างอิงไม่ได้ผูกพันกับภาษา มันเป็นกลยุทธ์การเชื่อมพารามิเตอร์ถัดจาก pass-by-value, pass by name, pass by need ฯลฯ ...

sidenote: ชื่อคลาสTestRefเป็นตัวเลือกที่ไม่ดีในบริบทนี้;)

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