อะไรคือความแตกต่างระหว่างการอ้างอิง C # และตัวชี้?


86

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

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

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

ฉันพลาดจุดใหญ่ไปหรือเปล่า?


1
คำตอบสั้น ๆ คือใช่คุณพลาดบางสิ่งที่สำคัญพอสมควรและนั่นคือสาเหตุของ "... การรับรู้ที่ผู้คนต้องเข้าใจคำชี้" คำแนะนำ: C # ไม่ใช่ภาษาเดียวที่มีอยู่
jdigital

คำตอบ:


51

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

แก้ไข: ตามแนวคิดใช่ จะเหมือนกันมากหรือน้อย


ไม่มีคำสั่งอื่นที่ป้องกันการอ้างอิง C # ไม่ให้วัตถุที่อ้างอิงถูกย้ายโดย GC หรือไม่?
Richard

ขออภัยฉันคิดว่าเป็นอย่างอื่นเนื่องจากโพสต์อ้างถึงตัวชี้
Richard

ใช่ GCHandle Alloc หรือ Marshal AllocHGlobal (เกินกำหนด)
ctacke

ได้รับการแก้ไขใน C #, pin_ptr ใน C ++ / CLI
mmx

Marshal AllocHGlobal จะไม่จัดสรรหน่วยความจำในฮีปที่มีการจัดการเลยและโดยปกติแล้วจะไม่ถูกเก็บรวบรวมขยะ
mmx

133

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

ยกตัวอย่างรหัสต่อไปนี้

int* p1 = GetAPointer();

ประเภทนี้ปลอดภัยในแง่ที่ GetAPointer ต้องส่งคืนประเภทที่เข้ากันได้กับ int * แต่ก็ยังไม่มีการรับประกันว่า * p1 จะชี้ไปที่ int จริง อาจเป็นอักขระสองตัวหรือเพียงแค่ตัวชี้ในหน่วยความจำสุ่ม

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

string str = GetAString();

ในกรณีนี้ str มีสถานะหนึ่งในสองสถานะ 1) ชี้ไปที่ไม่มีวัตถุและด้วยเหตุนี้จึงเป็นโมฆะหรือ 2) ชี้ไปยังสตริงที่ถูกต้อง แค่นั้นแหละ. CLR รับประกันว่าจะเป็นเช่นนั้น มันไม่สามารถและจะไม่เป็นตัวชี้


13

การอ้างอิงเป็นตัวชี้ "นามธรรม": คุณไม่สามารถคำนวณเลขคณิตโดยใช้ข้อมูลอ้างอิงได้และคุณไม่สามารถเล่นกลเม็ดระดับต่ำด้วยค่าของมันได้


8

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

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


5

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

นอกจากนี้พอยน์เตอร์ยังใช้ได้ในบริบทที่ไม่ปลอดภัยเท่านั้น


5

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

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


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

ถ้า p เป็น int * (ตัวชี้ไปที่ int) ดังนั้น (p + 1) คือแอดเดรสที่ระบุโดย p + 4 ไบต์ (ขนาดของ int) และ p [1] จะเหมือนกับ * (p + 1) (นั่นคือ "dereferences" ที่อยู่ 4 ไบต์ที่ผ่านมา p) ในทางตรงกันข้ามกับการอ้างอิงอาร์เรย์ (ใน C #) ตัวดำเนินการ [] จะเรียกใช้ฟังก์ชัน
P Daddy

5

ก่อนอื่นฉันคิดว่าคุณต้องกำหนด "ตัวชี้" ในสัญศาสตร์ของคุณ คุณหมายถึงตัวชี้ที่คุณสามารถสร้างในรหัสที่ไม่ปลอดภัยโดยแก้ไขหรือไม่? คุณหมายถึงIntPtrที่คุณได้รับจากอาจจะโทรพื้นเมืองหรือMarshal.AllocHGlobal ? คุณหมายถึงGCHandleหรือเปล่า? โดยพื้นฐานแล้วทั้งหมดเป็นสิ่งเดียวกันนั่นคือการแสดงที่อยู่หน่วยความจำที่มีการจัดเก็บบางสิ่งไม่ว่าจะเป็นคลาสตัวเลขโครงสร้างหรืออะไรก็ตาม และสำหรับการบันทึกนั้นพวกเขาสามารถอยู่ในกองได้อย่างแน่นอน

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

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

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

แก้ไข:ในความเป็นจริงนี่เป็นตัวอย่างที่ดีเมื่อคุณอาจใช้ "พอยน์เตอร์" ในโค้ดที่มีการจัดการ - ในกรณีนี้คือ GCHandle แต่สิ่งเดียวกันนั้นสามารถทำได้กับ AllocHGlobal หรือโดยใช้การแก้ไขบนอาร์เรย์ไบต์หรือโครงสร้าง ฉันมักจะชอบ GCHandle เพราะมันให้ความรู้สึก ".NET" มากกว่าสำหรับฉัน


การเล่นลิ้นเล็กน้อยที่บางทีคุณไม่ควรพูดว่า "ตัวชี้ที่มีการจัดการ" ที่นี่แม้จะมีเครื่องหมายคำพูดที่ทำให้ตกใจเพราะนี่เป็นสิ่งที่ค่อนข้างแตกต่างจากการอ้างอิงวัตถุใน IL แม้ว่าจะมีไวยากรณ์สำหรับพอยน์เตอร์ที่มีการจัดการใน C ++ / CLI แต่โดยทั่วไปไม่สามารถเข้าถึงได้จาก C # ใน IL จะได้รับตามคำแนะนำ (เช่น) ldloca และ ldarga
Glenn Slayden

5

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


1

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


1

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

พอยน์เตอร์มักถูกวิพากษ์วิจารณ์ว่า 'น่าเกลียด'

class* myClass = new class();

ตอนนี้ทุกครั้งที่คุณใช้คุณจะต้องยกเลิกการอ้างอิงก่อนโดย

myClass->Method() or (*myClass).Method()

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

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

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

การอ้างอิง C ++ แตกต่างจากการอ้างอิง C # / Java ตรงที่เมื่อคุณกำหนดค่าให้กับค่านั้นแล้วคุณจะไม่สามารถกำหนดใหม่ได้ (และจะต้องกำหนดเมื่อมีการประกาศ) สิ่งนี้เหมือนกับการใช้ตัวชี้ const (ตัวชี้ที่ไม่สามารถชี้ไปที่วัตถุอื่นอีกครั้งได้)

Java และ C # เป็นภาษาสมัยใหม่ระดับสูงมากซึ่งทำความสะอาดความยุ่งเหยิงจำนวนมากที่สะสมใน C / C ++ ในช่วงหลายปีที่ผ่านมาและคำแนะนำก็เป็นหนึ่งในสิ่งเหล่านั้นที่จำเป็นต้อง 'ทำความสะอาด'

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

ในตัวอย่างเฉพาะนี้การรู้อะไรเกี่ยวกับพอยน์เตอร์จะช่วยคุณในการอ้างอิง การทำความเข้าใจว่าการอ้างอิง C # ไม่ใช่วัตถุ แต่ชี้ไปที่วัตถุเป็นแนวคิดที่สำคัญมาก

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

# 2: ความหลากหลาย / อินเทอร์เฟซ เมื่อคุณมีการอ้างอิงที่เป็นประเภทอินเทอร์เฟซและชี้ไปที่วัตถุคุณสามารถเรียกใช้เมธอดของอินเทอร์เฟซนั้นได้เท่านั้นแม้ว่าออบเจ็กต์อาจมีความสามารถอื่น ๆ อีกมากมาย วัตถุอาจใช้วิธีการเดียวกันแตกต่างกัน

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

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


1
ส่วนตัวผมคิดว่า C # จะได้รับเป็นภาษาที่ดีกว่าถ้าสถานที่มากที่สุดที่ใช้.นำมาใช้->แต่ก็ตรงกันกับการเรียกร้องให้วิธีการคงที่foo.bar(123) fooClass.bar(ref foo, 123)ที่จะได้รับอนุญาตสิ่งต่างๆเช่นmyString.Append("George"); [ซึ่งจะปรับเปลี่ยนตัวแปร myString ] และทำให้ชัดเจนมากขึ้นความแตกต่างในความหมายระหว่างและmyStruct.field = 3; myClassObject->field = 3;
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.