เมื่อใดควรใช้การอ้างอิงและเมื่อไม่จำเป็นใน C #


104

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

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

มันทำให้ฉันสับสนเพราะทั้งสองอย่างreceived_sและremoteEPกำลังส่งคืนสิ่งต่างๆจากฟังก์ชัน ทำไมremoteEPต้องมีrefและreceived_sไม่?

ฉันเป็นโปรแกรมเมอร์ ac ด้วยดังนั้นฉันจึงมีปัญหาในการดึงตัวชี้ออกจากหัว

แก้ไข: ดูเหมือนว่าวัตถุใน C # เป็นตัวชี้ไปยังวัตถุที่อยู่ใต้ฝากระโปรง ดังนั้นเมื่อคุณส่งผ่านวัตถุไปยังฟังก์ชันคุณสามารถแก้ไขเนื้อหาของวัตถุผ่านตัวชี้และสิ่งเดียวที่ส่งผ่านไปยังฟังก์ชันคือตัวชี้ไปยังวัตถุเพื่อไม่ให้วัตถุนั้นถูกคัดลอก คุณใช้ ref หรือ out ถ้าคุณต้องการให้สามารถสลับออกหรือสร้างวัตถุใหม่ในฟังก์ชันซึ่งเปรียบเสมือนตัวชี้คู่

คำตอบ:


219

คำตอบสั้น: อ่านของฉันบทความเกี่ยวกับการโต้แย้งผ่าน

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

เมื่อพารามิเตอร์ (ชนิดใดก็ได้) ถูกส่งผ่านโดยการอ้างอิงนั่นหมายความว่าผู้เรียกใช้การเปลี่ยนแปลงใด ๆ กับพารามิเตอร์ - การเปลี่ยนแปลงพารามิเตอร์คือการเปลี่ยนแปลงของตัวแปร

บทความนี้อธิบายรายละเอียดทั้งหมดนี้โดยละเอียด :)

คำตอบที่เป็นประโยชน์: คุณแทบไม่ต้องใช้ ref / outเลย โดยพื้นฐานแล้วเป็นวิธีการรับค่าตอบแทนอื่นและโดยปกติควรหลีกเลี่ยงอย่างแม่นยำเพราะนั่นหมายความว่าวิธีนี้อาจพยายามทำมากเกินไป นั่นไม่ใช่กรณีเสมอไป ( TryParseฯลฯ เป็นตัวอย่างที่ยอมรับได้ของการใช้งานที่สมเหตุสมผลout) แต่การใช้ ref / out ควรเป็นสิ่งที่หายาก


39
ฉันคิดว่าคุณมีคำตอบสั้น ๆ และคำตอบแบบยาวปะปนกัน เป็นบทความใหญ่!
Outlaw Programmer

23
@Outlaw: ใช่ แต่คำตอบสั้น ๆ เองคำสั่งในการอ่านบทความมีความยาวเพียง 6 คำ :)
Jon Skeet

28
อ้างอิง! :)
gonzobrains

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

1
@ M.Mimpen: C # 7 จะ (หวังว่า) จะอนุญาตif (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet

26

คิดว่าพารามิเตอร์ non-ref เป็นตัวชี้และพารามิเตอร์ ref เป็นตัวชี้คู่ สิ่งนี้ช่วยฉันได้มากที่สุด

คุณแทบไม่ควรส่งผ่านค่าด้วยการอ้างอิง ฉันสงสัยว่าถ้าไม่ใช่เพราะความกังวลเกี่ยวกับการทำงานร่วมกันทีม. Net จะไม่รวมไว้ในข้อกำหนดเดิม วิธี OO ในการจัดการกับปัญหาส่วนใหญ่ที่พารามิเตอร์อ้างอิงแก้ไขได้คือ:

สำหรับค่าที่ส่งคืนหลายค่า

  • สร้างโครงสร้างที่แสดงถึงค่าที่ส่งคืนหลายค่า

สำหรับ primitives ที่เปลี่ยนวิธีการอันเป็นผลมาจากการเรียก method (method มีผลข้างเคียงกับพารามิเตอร์ primitive)

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

8
พระเจ้าที่ดี. ฉันต้องอ่าน 20x นี้ถึงจะเข้าใจ ดูเหมือนจะเป็นงานพิเศษสำหรับฉันเพียงเพื่อทำอะไรง่ายๆ
PositiveGuy

กรอบงาน. NET ไม่เป็นไปตามกฎ # 1 ของคุณ ('สำหรับค่าที่ส่งคืนหลายค่าให้สร้างโครงสร้าง') IPAddress.TryParse(string, out IPAddress)ใช้ตัวอย่างเช่น
Swen Kooij

@SwenKooij คุณพูดถูก ฉันสมมติว่าในสถานที่ส่วนใหญ่ที่พวกเขาใช้พารามิเตอร์เป็นอย่างใดอย่างหนึ่ง (ก) พวกเขากำลังห่อ Win32 API หรือ (b) เป็นช่วงแรก ๆ และโปรแกรมเมอร์ C ++ กำลังตัดสินใจ
Michael Meadows

@SwenKooij ฉันรู้ว่าการตอบกลับนี้ช้าไปหลายปี แต่ก็เป็นเช่นนั้น เราคุ้นเคยกับ TryParse แต่ก็ไม่ได้หมายความว่ามันดี มันจะดีกว่าถ้าif (int.TryParse("123", out var theInt) { /* use theInt */ }เรามีvar candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }โค้ดมากกว่า แต่สอดคล้องกับการออกแบบภาษา C # มากกว่า
Michael Meadows

10

คุณอาจเขียนแอป C # ทั้งหมดและไม่ส่งผ่านวัตถุ / โครงสร้างใด ๆ โดยการอ้างอิง

ฉันมีศาสตราจารย์ที่บอกฉันสิ่งนี้:

สถานที่เดียวที่คุณใช้อ้างอิงคือที่ที่คุณ:

  1. ต้องการส่งผ่านวัตถุขนาดใหญ่ (เช่นวัตถุ / โครงสร้างมีวัตถุ / โครงสร้างภายในไปยังหลายระดับ) และการคัดลอกจะมีราคาแพงและ
  2. คุณกำลังเรียกใช้ Framework, Windows API หรือ API อื่น ๆ ที่ต้องการ

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

ฉันเห็นด้วยกับคำแนะนำของเขาและในช่วงห้าปีที่ผ่านมาตั้งแต่เลิกเรียนฉันไม่เคยมีความต้องการนอกเหนือจากการเรียก Framework หรือ Windows API


3
หากคุณวางแผนที่จะใช้ "Swap" การส่งผ่าน ref อาจเป็นประโยชน์
Brian

@ คริสจะสร้างปัญหาหรือไม่ถ้าฉันใช้คีย์เวิร์ดอ้างอิงสำหรับวัตถุขนาดเล็ก?
ManirajSS

@TechnikEmpire "แต่การเปลี่ยนแปลงวัตถุภายในขอบเขตของฟังก์ชันที่เรียกนั้นจะไม่สะท้อนกลับไปยังผู้โทร" ว่าเป็นสิ่งที่ผิด. ถ้าฉันส่งบุคคลไปให้SetName(person, "John Doe")คุณสมบัติของชื่อจะเปลี่ยนไปและการเปลี่ยนแปลงนั้นจะแสดงให้ผู้โทรทราบ
M. Mimpen

@ M.Mimpen ความคิดเห็นถูกลบ ตอนนั้นฉันแทบไม่ได้หยิบ C # ขึ้นมาเลยและก็พูดถึง * $$ ของฉันอย่างชัดเจน ขอบคุณที่แจ้งให้เราทราบ

@ คริส - ฉันค่อนข้างมั่นใจว่าการส่งวัตถุ "ขนาดใหญ่" นั้นไม่แพง หากคุณเพียงแค่ส่งผ่านค่าคุณยังแค่ส่งตัวชี้เดียวใช่ไหม
เอียน

3

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

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

การอ้างอิงปลายทางกำลังอัปเดตตัวชี้ไปยังปลายทางในฟังก์ชันภายนอกเป็นอินสแตนซ์ใหม่ที่สร้างขึ้นภายในฟังก์ชัน


3

คิดว่าการอ้างอิงหมายถึงคุณกำลังส่งผ่านตัวชี้โดยการอ้างอิง การไม่ใช้การอ้างอิงหมายความว่าคุณกำลังส่งตัวชี้ตามค่า

ยังดีกว่าไม่สนใจสิ่งที่ฉันกล่าวว่า (มันอาจจะทำให้เข้าใจผิดโดยเฉพาะอย่างยิ่งกับประเภทค่า) และอ่านหน้านี้ MSDN


จริงไม่จริง. อย่างน้อยส่วนที่สอง ประเภทการอ้างอิงใด ๆ จะถูกส่งต่อโดยการอ้างอิงเสมอไม่ว่าคุณจะใช้การอ้างอิงหรือไม่ก็ตาม
Erik Funkenbusch

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

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

ลิงก์ตาย - ลองใช้อันนี้แทน - docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
GIVE-ME-CHICKEN

0

ความเข้าใจของฉันคืออ็อบเจ็กต์ทั้งหมดที่ได้รับจากคลาสอ็อบเจ็กต์จะถูกส่งผ่านเป็นพอยน์เตอร์ในขณะที่ชนิดธรรมดา (int, struct) ไม่ถูกส่งผ่านเป็นพอยน์เตอร์และต้องการการอ้างอิง ฉันไม่แน่ใจเกี่ยวกับสตริง (ท้ายที่สุดแล้วมันมาจากคลาส Object หรือไม่)


สิ่งนี้สามารถใช้คำชี้แจงได้ อะไรคือความแตกต่างระหว่างมูลค่าและการอ้างอิง ตอบเหตุใดจึงเกี่ยวข้องกับการใช้คำหลักอ้างอิงกับพารามิเตอร์
oɔɯǝɹ

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

1
เพื่อตอบคำถามของคุณ: สตริงมาจากวัตถุเป็นประเภทอ้างอิงที่ทำงานเหมือนประเภทค่า (สำหรับประสิทธิภาพและเหตุผลเชิงตรรกะ)
buddybubble

0

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

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


-1

กฎ Ground Zero ก่อน Primitives จะถูกส่งผ่านโดย value (stack) และ Non-Primitive โดยการอ้างอิง (Heap) ในบริบทของ TYPES ที่เกี่ยวข้อง

พารามิเตอร์ที่เกี่ยวข้องจะถูกส่งผ่านโดยค่าเริ่มต้น โพสต์ที่ดีที่อธิบายสิ่งต่างๆโดยละเอียด http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

เราสามารถพูดได้ว่า (พร้อมคำเตือน) ประเภทที่ไม่ใช่แบบดั้งเดิมนั้นไม่มีอะไรนอกจากตัวชี้ และเมื่อเราส่งผ่านโดยการอ้างอิงเราสามารถพูดได้ว่าเรากำลังส่งตัวชี้คู่


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

เห็นด้วย @Servy! "เมื่อเราได้ยินคำว่า" อ้างอิง "หรือ" ค่า "ที่ใช้เราควรจำไว้ให้ชัดเจนว่าเราหมายถึงพารามิเตอร์นั้นเป็นพารามิเตอร์อ้างอิงหรือพารามิเตอร์ค่าหรือเราหมายความว่าประเภทที่เกี่ยวข้องนั้นเป็นการอ้างอิงหรือประเภทค่า 'เล็กน้อย ความสับสนในด้านของฉันเมื่อฉันพูดกฎ Ground zero ก่อน Primitives จะถูกส่งผ่านด้วยค่า (stack) และ Non-Primitive โดยการอ้างอิง (Heap) ที่ฉันหมายถึงจาก TYPES ที่เกี่ยวข้องไม่ใช่พารามิเตอร์! เมื่อเราพูดถึงพารามิเตอร์คุณถูกต้อง พารามิเตอร์ทั้งหมดจะถูกส่งผ่านด้วยค่าโดยค่าเริ่มต้นใน C #
Surender Singh Malik

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

1
คุณส่งผ่านพารามิเตอร์โดยการอ้างอิงหรือตามค่า คำว่า "โดยการอ้างอิง" และ "ตามค่า" ไม่ได้ใช้เพื่ออธิบายว่าประเภทเป็นประเภทค่าหรือประเภทการอ้างอิง
Servy

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