เมื่อใดที่ฉันควรใช้ struct แทนที่จะเป็นคลาสใน C #


1390

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

ฉันเจอกฎเหล่านี้ที่นี่ :

  • struct ควรแสดงถึงค่าเดียว
  • struct ควรมี footprint หน่วยความจำน้อยกว่า 16 ไบต์
  • ไม่ควรเปลี่ยนแปลง struct หลังจากการสร้าง

กฎเหล่านี้ทำงานอย่างไร struct หมายถึงความหมายอะไร?


247
System.Drawing.Rectangleละเมิดกฎทั้งสามข้อนี้
ChrisW

4
มีเกมเชิงพาณิชย์ค่อนข้างน้อยเขียนใน C # ประเด็นก็คือพวกมันจะใช้สำหรับโค้ดที่ได้รับการปรับให้ดีที่สุด
BlackTigerX

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


4
@ChrisW ฉันเห็น แต่ค่าเหล่านั้นไม่ได้เป็นตัวแทนของสี่เหลี่ยมผืนผ้าซึ่งก็คือค่า "เดี่ยว"? เช่น Vector3D หรือ Color พวกมันก็มีหลายค่าอยู่ภายใน แต่ฉันคิดว่ามันเป็นค่าเดียวหรือเปล่า
Marson Mao

คำตอบ:


603

แหล่งที่มาที่อ้างอิงโดย OP มีความน่าเชื่อถือบางอย่าง ... แต่สิ่งที่เกี่ยวกับ Microsoft - ท่าทางในการใช้งาน struct คืออะไร? ฉันค้นหาการเรียนรู้เพิ่มเติมจาก Microsoftและนี่คือสิ่งที่ฉันพบ:

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

อย่ากำหนดโครงสร้างเว้นแต่ประเภทจะมีคุณสมบัติดังต่อไปนี้ทั้งหมด:

  1. มันมีเหตุผลแสดงค่าเดียวคล้ายกับประเภทดั้งเดิม (จำนวนเต็มคู่และอื่น ๆ )
  2. มันมีขนาดอินสแตนซ์ที่มีขนาดเล็กกว่า 16 ไบต์
  3. มันไม่เปลี่ยนรูป
  4. มันจะไม่ต้องมีกล่องบ่อย

Microsoft ละเมิดกฎเหล่านั้นอย่างสม่ำเสมอ

ตกลง # 2 และ # 3 ต่อไป พจนานุกรมอันเป็นที่รักของเรามีโครงสร้างภายใน 2 แบบ:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

* แหล่งอ้างอิง

แหล่งที่มา 'JonnyCantCode.com' ได้รับ 3 จาก 4 - ค่อนข้างลืมได้ตั้งแต่ # 4 อาจจะไม่เป็นปัญหา หากคุณพบว่าตัวเองต่อสู้โครงสร้างให้คิดใหม่สถาปัตยกรรมของคุณ

ลองดูว่าทำไม Microsoft จะใช้ structs เหล่านี้:

  1. แต่ละโครงสร้างEntryและEnumeratorแสดงถึงค่าเดียว
  2. ความเร็ว
  3. Entryไม่เคยผ่านเป็นพารามิเตอร์นอกคลาสพจนานุกรม การตรวจสอบเพิ่มเติมแสดงให้เห็นว่าเพื่อให้เป็นไปตามการนำ IEnumerable มาใช้พจนานุกรมใช้โครงสร้างEnumeratorที่คัดลอกทุกครั้งที่มีการร้องขอ enumerator ... เหมาะสม
  4. ภายในสู่คลาสพจนานุกรม Enumeratorเป็นสาธารณะเนื่องจากพจนานุกรมนับได้และต้องมีความสามารถในการเข้าถึงอินเทอร์เฟซ IEnumerator ที่เท่าเทียมกันเช่น IEnumerator getter

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

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

  1. ไม่มีการประกาศอะไรในโครงสร้างด้านบนreadonly- ไม่เปลี่ยนรูป
  2. ขนาดของโครงสร้างเหล่านี้อาจมีขนาดเกิน 16 ไบต์
  3. Entryมีเวลาชั่วชีวิตบึกบึน (จากAdd()ไปRemove(), Clear()หรือเก็บขยะ);

และ ... 4. โครงสร้างทั้งสองจัดเก็บ TKey และ TValue ซึ่งเราทุกคนรู้ว่ามีความสามารถในการเป็นประเภทอ้างอิง (เพิ่มข้อมูลโบนัส)

คีย์ที่แฮชแม้จะมีพจนานุกรมบางส่วนก็รวดเร็วเนื่องจากการวางโครงสร้างไว้นั้นเร็วกว่าประเภทอ้างอิง ที่นี่ฉันมีDictionary<int, int>ที่เก็บจำนวนเต็ม 300,000 สุ่มด้วยปุ่มเพิ่มขึ้นตามลำดับ

ความจุ: 312874
MemSize: 2660827 bytes
เสร็จสมบูรณ์ปรับขนาด: 5ms
เวลาทั้งหมดที่จะเติม: 889ms

ความจุ : จำนวนขององค์ประกอบที่มีอยู่ก่อนที่จะต้องปรับขนาดอาร์เรย์ภายใน

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

เสร็จสิ้นการปรับขนาด : เวลาที่ใช้ในการปรับขนาดอาร์เรย์ภายในจากองค์ประกอบ 150,862 เป็น 312874 องค์ประกอบ เมื่อคุณคิดว่าแต่ละองค์ประกอบถูกคัดลอกไปตามลำดับArray.CopyTo()นั่นจะไม่โทรมเกินไป

เวลาทั้งหมดในการกรอก : เบ้ยอมรับเนื่องจากการบันทึกและOnResizeเหตุการณ์ที่ฉันเพิ่มลงในแหล่งที่มา อย่างไรก็ตามยังคงน่าประทับใจในการเติมจำนวนเต็ม 300k ในขณะที่ปรับขนาด 15 ครั้งในระหว่างการดำเนินการ แค่อยากรู้อยากเห็นอะไรที่จะเติมเต็มถ้าฉันรู้ความสามารถแล้ว? 13ms

ดังนั้นตอนนี้ถ้าEntryเป็นคลาสล่ะ เวลาหรือตัวชี้วัดเหล่านี้จะแตกต่างกันมาก

ความจุ: 312874
MemSize: 2660827 bytes
เสร็จสมบูรณ์ปรับขนาด: 26ms
เวลาทั้งหมดที่จะเติม: 964ms

เห็นได้ชัดว่าความแตกต่างใหญ่คือในการปรับขนาด ความแตกต่างใด ๆ หากพจนานุกรมถูกกำหนดค่าเริ่มต้นด้วยความจุ ไม่พอที่จะเกี่ยวข้องกับ ... 12ms

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

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

เหตุผลที่ฉันต้องเริ่มต้นแต่ละองค์ประกอบของอาร์เรย์Entryเป็นชนิดอ้างอิงสามารถพบได้ที่MSDN: การออกแบบโครงสร้าง ในระยะสั้น:

ไม่ต้องจัดให้มีการสร้างเริ่มต้นสำหรับโครงสร้าง

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

คอมไพเลอร์บางตัวเช่นคอมไพเลอร์ C # ไม่อนุญาตให้โครงสร้างมีตัวสร้างเริ่มต้น

จริงๆแล้วมันค่อนข้างง่ายและเราจะยืมจากThree Laws of Robotics ของ Asimov :

  1. โครงสร้างต้องปลอดภัยที่จะใช้
  2. โครงสร้างต้องปฏิบัติหน้าที่ได้อย่างมีประสิทธิภาพเว้นแต่จะเป็นการละเมิดกฎ # 1
  3. โครงสร้างต้องยังคงไม่บุบสลายระหว่างการใช้งานเว้นแต่จะต้องทำลายมันเพื่อให้เป็นไปตามกฎ # 1

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


8
สำหรับกฎของ Microsoft กฎเกี่ยวกับการเปลี่ยนแปลงไม่ได้ดูเหมือนว่าจะได้รับการออกแบบมาเพื่อกีดกันการใช้ประเภทค่าในลักษณะที่พฤติกรรมของพวกเขาจะแตกต่างจากประเภทอ้างอิงแม้ว่าจะมีความหมายว่าค่าความหมายที่ไม่แน่นอนในแต่ละชิ้นนั้นมีประโยชน์ก็ตาม หากมีชนิดที่ไม่แน่นอนสามารถทำให้ง่ายต่อการทำงานและถ้าตำแหน่งที่เก็บข้อมูลประเภทนั้นควรแยกออกจากกันทางตรรกะประเภทควรเป็นโครงสร้าง "ไม่แน่นอน"
supercat


2
ความจริงที่ว่า Microsoft หลายประเภทละเมิดกฎเหล่านั้นไม่ได้แสดงถึงปัญหากับประเภทเหล่านั้น แต่แสดงว่ากฎไม่ควรใช้กับโครงสร้างทุกประเภท หากโครงสร้างแสดงถึงเอนทิตีเดี่ยว [เช่นเดียวกับDecimalหรือDateTime] ดังนั้นหากมันจะไม่ปฏิบัติตามกฎสามข้ออื่น ๆ มันควรจะถูกแทนที่ด้วยคลาส หากโครงสร้างมีการเก็บรวบรวมตัวแปรที่คงที่ซึ่งแต่ละค่าอาจเก็บค่าใด ๆ ที่จะถูกต้องสำหรับประเภทของมัน [เช่นRectangle] จากนั้นก็ควรปฏิบัติตามกฎที่แตกต่างกัน .
supercat

4
@IAbstract: บางคนจะปรับDictionaryประเภทรายการโดยพิจารณาจากประเภทภายในเท่านั้นประสิทธิภาพถือว่ามีความสำคัญมากกว่าความหมายหรือข้อแก้ตัวอื่น ๆ จุดของฉันคือว่าประเภทเช่นRectangleควรจะมีเนื้อหาเปิดเผยว่าเขตข้อมูลเป็นรายบุคคลที่แก้ไขไม่ได้ "เพราะ" ผลประโยชน์เกินดุลที่เกิดความไม่สมบูรณ์ของความหมาย แต่เป็นเพราะชนิดความหมายหมายถึงการกำหนดชุดของค่าที่เป็นอิสระและเพื่อ struct ไม่แน่นอนเป็นทั้ง performant มากขึ้นและมีความหมายที่ดีกว่า
supercat

2
@supercat: ฉันเห็นด้วย ... และจุดทั้งหมดของคำตอบของฉันคือ 'แนวทาง' อ่อนสวยและ structs ควรใช้กับความรู้และความเข้าใจอย่างเต็มรูปแบบของพฤติกรรม ดูคำตอบของฉันเกี่ยวกับโครงสร้างที่ไม่แน่นอนที่นี่: stackoverflow.com/questions/8108920/…
IAbstract

155

เมื่อไหร่ก็ตามที่คุณ:

  1. ไม่ต้องการความหลากหลาย
  2. ต้องการซีแมนทิกส์คุณค่าและ
  3. ต้องการหลีกเลี่ยงการจัดสรรฮีปและโอเวอร์เฮดการรวบรวมขยะที่เกี่ยวข้อง

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


1
นั่นเป็นเพียง "ข้อแม้" เท่านั้น ควรพิจารณา "การยก" ประเภทมูลค่าและกรณีเช่น(Guid)null(มันก็โอเคที่จะโยนเป็นโมฆะเป็นประเภทอ้างอิง) เหนือสิ่งอื่นใด

1
แพงกว่าใน C / C ++ ไหม ใน C ++ วิธีที่แนะนำคือการส่งผ่านวัตถุตามค่า
Ion Todirel

@IonTodirel นั่นไม่ได้เป็นเพราะเหตุผลด้านความปลอดภัยของหน่วยความจำมากกว่าประสิทธิภาพหรือไม่? มันเป็นการแลกเปลี่ยนที่ดีเสมอ แต่การผ่าน 32 B โดยสแต็กนั้นจะช้ากว่าการอ้างอิง 4 B โดยการลงทะเบียน อย่างไรก็ตามโปรดทราบว่าการใช้ "ค่า / การอ้างอิง" นั้นแตกต่างกันเล็กน้อยใน C # และ C ++ - เมื่อคุณส่งการอ้างอิงไปยังวัตถุคุณยังคงผ่านคุณค่าแม้ว่าคุณจะผ่านการอ้างอิง (คุณ ' กำลังส่งค่าการอ้างอิงไม่ใช่การอ้างอิงถึงการอ้างอิงโดยทั่วไป) มันไม่ใช่ความหมายตามตัวอักษรแต่ในทางเทคนิคแล้วมันเป็น "pass-by-value"
Luaan

@Luaan Copy เป็นเพียงค่าใช้จ่ายด้านเดียว ทางอ้อมพิเศษเนื่องจากตัวชี้ / อ้างอิงยังค่าใช้จ่ายต่อการเข้าถึง ในบางกรณีสามารถย้ายโครงสร้างได้ดังนั้นจึงไม่จำเป็นต้องคัดลอก
Onur

@ พวกเราน่าสนใจ คุณ "ย้าย" โดยไม่คัดลอกได้อย่างไร ฉันคิดว่าคำสั่ง asm "mov" ไม่ใช่ "ย้าย" มันคัดลอก
อนุรักษ์นิยม Sendon

148

ฉันไม่เห็นด้วยกับกฎที่ให้ไว้ในโพสต์ต้นฉบับ นี่คือกฎของฉัน:

1) คุณใช้ structs เพื่อประสิทธิภาพเมื่อเก็บไว้ในอาร์เรย์ (โปรดดูคำตอบเมื่อโครงสร้างมีคำตอบเมื่อใด )

2) คุณต้องการรหัสในการส่งข้อมูลที่มีโครงสร้างไปยัง / จาก C / C ++

3) อย่าใช้ structs เว้นแต่คุณต้องการ:

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

4
+1 ใช่ฉันเห็นด้วยกับ # 1 ทั้งหมด (นี่เป็นข้อได้เปรียบอย่างมากเมื่อจัดการกับสิ่งต่าง ๆ เช่นรูปภาพ ฯลฯ ) และสำหรับการชี้ให้เห็นว่าพวกเขาแตกต่างจาก "วัตถุปกติ" และมีวิธีรู้เรื่องนี้ยกเว้นความรู้ที่มีอยู่ หรือตรวจสอบประเภทของตัวเอง นอกจากนี้คุณไม่สามารถแปลงค่า Null เป็นประเภท struct :-) นี่เป็นกรณีหนึ่งที่ฉันเกือบอยากมี 'ฮังการี' บางอย่างสำหรับประเภทที่ไม่ใช่ค่าหลักหรือคำหลัก 'struct' บังคับที่ไซต์ประกาศตัวแปร .

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

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

จุดที่มี # 1 รายการที่เต็มไปด้วย structs สามารถบีบข้อมูลที่เกี่ยวข้องมากขึ้นลงในแคช L1 / L2 กว่ารายการที่เต็มไปด้วยการอ้างอิงวัตถุ (สำหรับโครงสร้างขนาดที่เหมาะสม)
Matt Stephenson

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

88

ใช้ struct เมื่อคุณต้องการซีแมนทิกส์คุณค่าซึ่งตรงข้ามกับซีแมนทิกส์อ้างอิง

แก้ไข

ไม่แน่ใจว่าทำไมคนเหล่านี้ถึงลง แต่นี่เป็นจุดที่ถูกต้องและทำก่อนที่ op จะชี้แจงคำถามของเขาและมันเป็นเหตุผลพื้นฐานที่สำคัญที่สุดสำหรับ struct

ถ้าคุณต้องการซีแมนทิกส์อ้างอิงคุณต้องมีคลาสไม่ใช่โครงสร้าง


13
ทุกคนรู้ดีว่า ดูเหมือนว่าเขากำลังมองหาคำตอบ "struct is a value value" มากกว่า
TheSmurf

21
เป็นกรณีพื้นฐานที่สุดและควรระบุไว้สำหรับทุกคนที่อ่านโพสต์นี้และไม่ทราบว่าเป็นเช่นนั้น
JoshBerke

3
ไม่ใช่ว่าคำตอบนี้ไม่เป็นความจริง เห็นได้ชัดว่ามันคือ นั่นไม่ใช่ประเด็นจริงๆ
TheSmurf

55
@Josh: สำหรับใครก็ตามที่ไม่รู้จักก็แค่พูดว่ามันเป็นคำตอบที่ไม่เพียงพอเพราะมันค่อนข้างเป็นไปได้ที่พวกเขาจะไม่รู้ว่ามันแปลว่าอะไร
TheSmurf

1
ฉันเพิ่ง downvote นี้เพราะฉันคิดว่าหนึ่งในคำตอบอื่น ๆ ควรจะอยู่ด้านบน - คำตอบใด ๆ ที่ระบุว่า "สำหรับการทำงานร่วมกันกับรหัสที่ไม่มีการจัดการมิฉะนั้นหลีกเลี่ยง"
Daniel Earwicker

59

นอกจากคำตอบ "เป็นค่า" แล้วสถานการณ์เฉพาะอย่างหนึ่งสำหรับการใช้ structs คือเมื่อคุณรู้ว่าคุณมีชุดข้อมูลที่ทำให้เกิดปัญหาการรวบรวมขยะและคุณมีวัตถุจำนวนมาก ตัวอย่างเช่นรายการ / อาร์เรย์ขนาดใหญ่ของอินสแตนซ์บุคคล อุปมาที่เป็นธรรมชาติที่นี่เป็นคลาส แต่ถ้าคุณมีอินสแตนซ์บุคคลที่มีอายุยืนจำนวนมากพวกเขาสามารถจบลงด้วยการอุดตัน GEN-2 และทำให้แผงลอย GC หากใบสำคัญแสดงสิทธิที่สถานการณ์มันอาจเกิดขึ้นที่นี่วิธีการหนึ่งคือการใช้อาร์เรย์ (ไม่ใช่รายการ) ของผู้ที่structsPerson[]คือ ตอนนี้แทนที่จะมีวัตถุนับล้านใน GEN-2 คุณมีอันเดียวบน LOH (ฉันสมมติว่าไม่มีสตริง ฯลฯ ที่นี่ - นั่นคือคุณค่าที่บริสุทธิ์โดยไม่มีการอ้างอิงใด ๆ ) สิ่งนี้มีผลกระทบ GC น้อยมาก

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

int index = ...
int id = peopleArray[index].Id;

โปรดทราบว่าการรักษาค่าที่ไม่เปลี่ยนรูปจะช่วยได้ที่นี่ สำหรับตรรกะที่ซับซ้อนมากขึ้นให้ใช้วิธีการที่มีพารามิเตอร์ by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

อีกครั้งนี่คือแบบแทนที่เรายังไม่ได้คัดลอกค่า

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


+1 คำตอบที่น่าสนใจ คุณยินดีที่จะแบ่งปันเกร็ดเล็กเกร็ดน้อยในโลกแห่งความเป็นจริงเกี่ยวกับวิธีการดังกล่าวหรือไม่
Jordão

@ Jordao ในมือถือ แต่ค้นหา google สำหรับ: + gravell + "assault by GC"
Marc Gravell

1
ขอบคุณมาก. ผมพบว่ามันนี่
Jordão

2
@MarcGravell ทำไมคุณถึงพูดถึง: ใช้อาร์เรย์ (ไม่ใช่รายการ) ? Listฉันเชื่อว่าใช้Arrayฉากเบื้องหลัง ไม่นะ
Royi Namir

4
@RoyiNamir ฉันอยากรู้เกี่ยวกับสิ่งนี้เช่นกัน แต่ฉันเชื่อว่าคำตอบนั้นอยู่ในคำตอบของวรรคสอง "อย่างไรก็ตามการเข้าถึงโดยตรงในอาเรย์ไม่ได้คัดลอกโครงสร้างมันอยู่ในสถานที่ (ตรงกันข้ามกับตัวทำดัชนีรายการซึ่งคัดลอก)"
user1323245

40

จากข้อกำหนดภาษา C # :

1.7 โครงสร้าง

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

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

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

อีกทางเลือกหนึ่งคือทำให้ Point เป็น struct

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

ตอนนี้มีเพียงวัตถุเดียวเท่านั้นที่ถูกยกตัวอย่าง - วัตถุหนึ่งสำหรับอาเรย์ - และอินสแตนซ์ของจุดจะถูกจัดเก็บในบรรทัดในอาเรย์

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

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

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

ถ้า Point เป็นคลาสผลลัพธ์คือ 20 เนื่องจาก a และ b อ้างอิงถึงวัตถุเดียวกัน ถ้า Point เป็นโครงสร้างเอาต์พุตคือ 10 เนื่องจากการกำหนด a ถึง b สร้างสำเนาของค่าและสำเนานี้ไม่ได้รับผลกระทบจากการกำหนดต่อไปยัง ax

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


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

4
... ที่จะช่วยให้ความหมายที่เป็นประโยชน์ของ structs ผ่านrefไปได้ด้วยวัตถุคลาส โดยพื้นฐานแล้วตัวแปรโลคัลพารามิเตอร์และค่าส่งคืนฟังก์ชั่นอาจยังคงอยู่ (ค่าเริ่มต้น) ส่งคืนได้หรือชั่วคราว รหัสจะถูกห้ามมิให้คัดลอกสิ่งที่ไม่ยั่งยืนไปยังสิ่งใดก็ตามที่เกินขอบเขตขอบเขตปัจจุบัน สิ่งที่ส่งคืนได้จะเป็นสิ่งที่ไม่ยั่งยืนยกเว้นว่าสามารถส่งคืนจากฟังก์ชันได้ ค่าส่งคืนของฟังก์ชั่นจะถูกผูกไว้ด้วยข้อ จำกัด ที่เข้มงวดที่สุดที่ใช้กับพารามิเตอร์ใด ๆ ของ "คืนได้"
supercat

34

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


4
ใช่ แต่โครงสร้างขนาดใหญ่อาจมีราคาแพงกว่าการอ้างอิงชั้นเรียน (เมื่อผ่านไปยังวิธีการ)
Alex

27

นี่คือกฎพื้นฐาน

  • ถ้าเขตข้อมูลสมาชิกทุกประเภทค่าสร้างstruct

  • ถ้าเขตสมาชิกคนใดคนหนึ่งเป็นชนิดอ้างอิงสร้างระดับ นี่เป็นเพราะเขตข้อมูลประเภทการอ้างอิงจะต้องจัดสรรฮีปอยู่แล้ว

Exmaples

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}

3
ประเภทการอ้างอิงที่ไม่เปลี่ยนรูปแบบเช่นstringนั้นมีความหมายเทียบเท่ากับค่าและการจัดเก็บการอ้างอิงไปยังวัตถุที่ไม่เปลี่ยนรูปได้ลงในเขตข้อมูลจะไม่ทำให้เกิดการจัดสรรฮีป ความแตกต่างระหว่างโครงสร้างกับสัมผัสทุ่งประชาชนและวัตถุชั้นที่มีสัมผัสของประชาชนเป็นเขตที่ได้รับลำดับรหัสvar q=p; p.X=4; q.X=5;, p.Xจะมีค่า 4 ถ้าaเป็นชนิดโครงสร้างและ 5 หากเป็นประเภทระดับ หากมีความประสงค์ที่จะสามารถปรับเปลี่ยนสมาชิกของประเภทได้อย่างสะดวกคุณควรเลือก 'คลาส' หรือ 'โครงสร้าง' โดยขึ้นอยู่กับว่าต้องการเปลี่ยนแปลงqเพื่อส่งผลกระทบpหรือไม่
supercat

ใช่ฉันเห็นด้วยว่าตัวแปรอ้างอิงจะอยู่ในสแต็ก แต่วัตถุที่อ้างถึงจะมีอยู่บนฮีป แม้ว่า structs และคลาสจะทำงานแตกต่างกันเมื่อกำหนดให้กับตัวแปรที่แตกต่างกัน แต่ฉันไม่คิดว่าเป็นปัจจัยในการตัดสินใจที่ดี
Usman Zafar

structable ที่ไม่แน่นอนและคลาสที่ไม่แน่นอนจะทำงานแตกต่างอย่างสิ้นเชิง; ถ้าสิ่งหนึ่งถูกต้องอีกฝ่ายหนึ่งน่าจะผิด ฉันไม่แน่ใจว่าพฤติกรรมจะไม่เป็นปัจจัยในการตัดสินใจว่าจะใช้ struct หรือคลาส
supercat

ฉันบอกว่ามันไม่ได้เป็นปัจจัยในการตัดสินใจที่แข็งแกร่งเพราะบ่อยครั้งเมื่อคุณสร้างชั้นเรียนหรือโครงสร้างคุณไม่แน่ใจว่าจะใช้งานอย่างไร ดังนั้นคุณจึงสนใจว่าสิ่งต่าง ๆ มีความหมายมากขึ้นจากมุมมองของการออกแบบอย่างไร อย่างไรก็ตามฉันไม่เคยเห็นที่เดียวในห้องสมุด. NET ที่ struct มีตัวแปรอ้างอิง
Usman Zafar

1
ชนิดของโครงสร้างArraySegment<T>ห่อหุ้ม a T[]ซึ่งเป็นชนิดคลาสเสมอ ประเภทโครงสร้างKeyValuePair<TKey,TValue>มักใช้กับประเภทคลาสเป็นพารามิเตอร์ทั่วไป
supercat

19

ก่อน: สถานการณ์ Interop หรือเมื่อคุณต้องการระบุเค้าโครงหน่วยความจำ

ที่สอง: เมื่อข้อมูลมีขนาดใกล้เคียงกับตัวชี้อ้างอิงอยู่แล้ว


17

คุณต้องใช้ "struct" ในสถานการณ์ที่คุณต้องการระบุเค้าโครงหน่วยความจำอย่างชัดเจนโดยใช้StructLayoutAttribute - โดยทั่วไปสำหรับ PInvoke

แก้ไข: ความคิดเห็นชี้ให้เห็นว่าคุณสามารถใช้คลาสหรือ struct ด้วย StructLayoutAttribute และนั่นเป็นความจริงอย่างแน่นอน ในทางปฏิบัติคุณมักจะใช้ struct - มันถูกจัดสรรใน stack vs heap ซึ่งเหมาะสมถ้าคุณเพียงแค่ส่งอาร์กิวเมนต์ไปยังการเรียกเมธอดที่ไม่มีการจัดการ


5
StructLayoutAttribute สามารถนำไปใช้กับ structs หรือคลาสดังนั้นนี่ไม่ใช่เหตุผลที่จะใช้ structs
สตีเฟ่นมาร์ติน

เหตุใดจึงสมเหตุสมผลถ้าคุณเพิ่งผ่านการโต้แย้งไปยังการเรียกเมธอดที่ไม่มีการจัดการ
David Klempfner

16

ฉันใช้ structs สำหรับการบรรจุหรือการเอาออกของรูปแบบการสื่อสารไบนารีใด ๆ ซึ่งรวมถึงการอ่านหรือเขียนไปยังดิสก์รายการ DirectX vertex โปรโตคอลเครือข่ายหรือการจัดการกับข้อมูลที่เข้ารหัส / บีบอัด

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


คุณไม่สามารถใช้ประเภทการอ้างอิงสำหรับสิ่งนั้นได้อย่างง่ายดายหรือ
David Klempfner

15

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

  1. เมื่อคุณต้องการสำเนาความหมาย
  2. เมื่อคุณต้องการเริ่มต้นอัตโนมัติโดยปกติจะอยู่ในอาร์เรย์ของประเภทเหล่านี้

# 2 น่าจะเป็นส่วนหนึ่งของเหตุผลสำหรับความชุก struct ในสุทธิเรียนคอลเลกชัน ..
IAbstract

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

13

.NET รองรับvalue typesและreference types(ใน Java คุณสามารถกำหนดประเภทการอ้างอิงเท่านั้น) อินสแตนซ์ของการreference typesได้รับการจัดสรรในฮีปที่ได้รับการจัดการและเป็นขยะที่รวบรวมเมื่อไม่มีการอ้างอิงที่โดดเด่นถึงพวกเขา อินสแตนซ์ของvalue typesจะถูกจัดสรรในstackและดังนั้นหน่วยความจำที่จัดสรรจะถูกเรียกคืนทันทีที่ขอบเขตสิ้นสุด และแน่นอนvalue typesรับค่าและreference typesอ้างอิง ชนิดข้อมูลดั้งเดิม C # ทั้งหมดยกเว้น System.String เป็นประเภทค่า

เมื่อใดควรใช้ struct มากกว่าคลาส

ใน C #, structsมีคลาสvalue types reference typesคุณสามารถสร้างประเภทค่าใน C # โดยใช้enumคำหลักและstructคำหลัก การใช้ a value typeแทนที่จะเป็น a reference typeจะส่งผลให้มีวัตถุน้อยลงในฮีปที่ได้รับการจัดการซึ่งส่งผลให้โหลดน้อยลงในตัวรวบรวมขยะ (GC) รอบ GC ที่ใช้บ่อยน้อยลงและประสิทธิภาพที่ดีขึ้นตามลำดับ อย่างไรก็ตามvalue typesมีข้อเสียของพวกเขาด้วย การผ่านไปรอบ ๆstructนั้นมีค่าใช้จ่ายสูงกว่าการผ่านการอ้างอิงนั่นเป็นปัญหาที่ชัดเจน ปัญหาอื่น ๆ boxing/unboxingที่เกี่ยวข้องกับค่าใช้จ่าย ในกรณีที่คุณสงสัยว่าboxing/unboxingหมายถึงอะไรให้ทำตามลิงก์เหล่านี้เพื่อคำอธิบายที่ดีboxingและunboxing. นอกเหนือจากประสิทธิภาพแล้วยังมีบางครั้งที่คุณต้องการชนิดเพื่อให้มีความหมายตามตัวอักษรซึ่งอาจเป็นเรื่องยากมาก (หรือน่าเกลียด) ที่จะนำไปใช้หากreference typesคุณมีทั้งหมด คุณควรใช้value typesเฉพาะเมื่อคุณต้องการสำเนาความหมายหรือต้องการเริ่มต้นอัตโนมัติตามปกติในarraysประเภทเหล่านี้


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

... (2) เมื่อด้วยเหตุผลต่าง ๆ struct จะไม่สามารถใช้งานได้ [เช่นเพราะต้องใช้การอ้างอิงแบบซ้อนสำหรับบางสิ่งบางอย่างเช่นต้นไม้หรือเพราะต้องการ polymorphism] โปรดทราบว่าเมื่อใช้ประเภทค่าโดยทั่วไปแล้วควรเปิดเผยเขตข้อมูลโดยตรงโดยไม่มีเหตุผลเฉพาะที่จะไม่ (ในขณะที่เขตข้อมูลประเภทชั้นเรียนส่วนใหญ่ควรถูกห่อภายในคุณสมบัติ) หลายคนจึงเรียกว่า "ความชั่วร้าย" ของประเภทค่าไม่แน่นอนเกิดจากการตัดความจำเป็นของเขตข้อมูลในคุณสมบัติของ (เช่นในขณะที่คอมไพเลอร์บางคนจะให้หนึ่งในการเรียกหมาทรัพย์สินใน struct อ่านอย่างเดียวเพราะมันจะบางครั้ง ...
SuperCat

... ทำสิ่งที่ถูกต้องคอมไพเลอร์ทุกคนจะปฏิเสธความพยายามที่จะตั้งค่าฟิลด์ในโครงสร้างดังกล่าวโดยตรง วิธีที่ดีที่สุดเพื่อให้แน่ใจว่าคอมไพเลอร์ปฏิเสธreadOnlyStruct.someMember = 5;ไม่ได้สร้างคุณสมบัติsomeMemberแบบอ่านอย่างเดียว แต่ให้เป็นฟิลด์
supercat

12

structเป็นประเภทค่า หากคุณกำหนด struct ให้กับตัวแปรใหม่ตัวแปรใหม่จะมีสำเนาของต้นฉบับ

public struct IntStruct {
    public int Value {get; set;}
}

ผลที่ตามมาของผลลัพธ์ต่อไปนี้ใน5 อินสแตนซ์ของโครงสร้างที่เก็บไว้ในหน่วยความจำ:

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

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

public class IntClass {
    public int Value {get; set;}
}

ข้อยกเว้นของผลลัพธ์ต่อไปนี้ในอินสแตนซ์เดียวของวัตถุคลาสในหน่วยความจำ

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

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

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1

12

ฉันสร้างเกณฑ์มาตรฐานขนาดเล็กกับBenchmarkDotNetเพื่อทำความเข้าใจเกี่ยวกับประโยชน์ของ "struct" เป็นตัวเลข ฉันกำลังทดสอบการวนลูปผ่านอาร์เรย์ (หรือรายการ) ของ structs (หรือคลาส) การสร้างอาร์เรย์หรือรายการเหล่านั้นอยู่นอกขอบเขตของเกณฑ์มาตรฐาน - เป็นที่ชัดเจนว่า "คลาส" หนักกว่าจะใช้หน่วยความจำมากขึ้นและจะเกี่ยวข้องกับ GC

ดังนั้นข้อสรุปคือ: ระวังด้วย LINQ และโครงสร้างที่ซ่อนชกมวย / unboxing และใช้ structs สำหรับ microoptimizations อย่างเคร่งครัดอยู่กับอาร์เรย์

PS มาตรฐานอื่น ๆ เกี่ยวกับการส่ง struct / class ผ่าน call stack มีhttps://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

รหัส:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }

คุณคิดหรือไม่ว่าเหตุใด structs จึงช้ากว่านี้มากเมื่อใช้ในรายการและเช่นนั้น? มันเป็นเพราะการชกมวยที่ซ่อนอยู่และการแกะกล่องที่คุณพูดถึง? ถ้าเป็นเช่นนั้นทำไมมันเกิดขึ้น?
Marko Grdinic

การเข้าถึง struct ในอาเรย์ควรเร็วขึ้นเพราะไม่จำเป็นต้องมีการอ้างอิงเพิ่มเติม Boxing / Unboxing เป็นกรณีของ linq
Roman Pokrovskij

10

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

โปรดทราบว่ามีความเป็นไปได้ที่จะออกแบบประเภทโครงสร้างเพื่อให้มันทำตัวเหมือนคลาสประเภทนั้น ๆ หากโครงสร้างมีฟิลด์ประเภทคลาสส่วนตัวและเปลี่ยนเส้นทางสมาชิกของตัวเองไปยังคลาสอ็อบเจ็กต์ที่ถูกห่อ ยกตัวอย่างเช่นPersonCollectionคุณสมบัติอาจมีSortedByNameและSortedByIdซึ่งทั้งสองถือเป็น "ไม่เปลี่ยนรูป" อ้างอิงถึงPersonCollection(ตั้งค่าในตัวสร้างของพวกเขา) และดำเนินการGetEnumeratorโดยการเรียกอย่างใดอย่างหนึ่งหรือcreator.GetNameSortedEnumerator creator.GetIdSortedEnumeratorstructs ดังกล่าวจะมีพฤติกรรมเหมือนการอ้างอิงไปยังPersonCollectionยกเว้นว่าพวกเขาวิธีการจะถูกผูกไว้กับวิธีการที่แตกต่างกันในGetEnumerator PersonCollectionเราสามารถมีโครงสร้างห่อส่วนหนึ่งของอาร์เรย์ (เช่นหนึ่งสามารถกำหนดArrayRange<T>โครงสร้างซึ่งจะถือT[]เรียกว่าArrเป็น int Offsetและ intLengthด้วยคุณสมบัติดัชนีซึ่งสำหรับดัชนีidxในช่วง 0 ถึงLength-1จะเข้าถึงArr[idx+Offset]) แต่ถ้าfooเป็นเช่นอ่านอย่างเดียวของโครงสร้างดังกล่าวปัจจุบันรุ่นคอมไพเลอร์จะไม่อนุญาตให้ดำเนินการเช่นเพราะพวกเขามีวิธีการตรวจสอบว่าการดำเนินการดังกล่าวจะพยายามที่จะเขียนไปยังทุ่งไม่มีfoo[3]+=4;foo

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


10

Nah - ฉันไม่เห็นด้วยกับกฎทั้งหมด เป็นแนวทางที่ดีในการพิจารณาเกี่ยวกับประสิทธิภาพและมาตรฐาน แต่ไม่ใช่ในแง่ของความเป็นไปได้

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

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

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

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


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

หากข้อกำหนดสำหรับประเภท "จุด 3d" บ่งชี้ว่าสถานะทั้งหมดถูกเปิดเผยผ่านสมาชิกที่อ่านได้ x, y, และ z และเป็นไปได้ที่จะสร้างอินสแตนซ์ด้วยการรวมกันของdoubleค่าสำหรับพิกัดเหล่านั้นข้อมูลจำเพาะจะบังคับให้ ประพฤติ semantically เหมือนกันกับการเปิดเผย - ฟิลด์ struct ยกเว้นรายละเอียดของพฤติกรรมหลายเธรด (ที่ไม่เปลี่ยนรูปชั้นจะดีกว่าในบางกรณีในขณะที่การเปิดเผย - ฟิลด์ struct จะดีกว่าในคนอื่น ๆ ; จะแย่ลงในทุกกรณี)
supercat

8

กฎของฉันคือ

1 ใช้คลาสเสมอ

2, หากมีปัญหาด้านประสิทธิภาพใด ๆ ฉันพยายามที่จะเปลี่ยนคลาสเป็นโครงสร้างขึ้นอยู่กับกฎที่ @IAbstract กล่าวถึงจากนั้นทำการทดสอบเพื่อดูว่าการเปลี่ยนแปลงเหล่านี้สามารถปรับปรุงประสิทธิภาพได้หรือไม่


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

1
@supercat: ฉันคิดว่ามันไม่ยุติธรรมเลยที่จะตำหนิ Microsoft สำหรับเรื่องนี้ ปัญหาจริงที่นี่คือ C # ในฐานะที่เป็นภาษาเชิงวัตถุนั้นไม่ได้มุ่งเน้นไปที่ประเภทของเรกคอร์ดธรรมดาที่เปิดเผยข้อมูลโดยไม่มีพฤติกรรมมากเท่านั้น C # ไม่ใช่ภาษาแบบหลายกระบวนทัศน์ในระดับเดียวกับ C ++ ที่ถูกกล่าวว่าฉันยังเชื่อว่าน้อยคนโปรแกรมบริสุทธิ์ OOP ดังนั้น C # อาจเป็นภาษาในอุดมคติเกินไป (ฉันเพิ่งเริ่มเปิดเผยpublic readonlyฟิลด์ในประเภทของฉันเช่นกันเนื่องจากการสร้างคุณสมบัติแบบอ่านอย่างเดียวเป็นงานมากเกินไปที่จะไม่ก่อให้เกิดประโยชน์)
stakx - ไม่สนับสนุน

1
@stakx: ไม่จำเป็นต้อง "มุ่งเน้น" กับประเภทเหล่านี้ ตระหนักถึงสิ่งที่พวกเขาจะพอเพียง จุดอ่อนที่ใหญ่ที่สุดของ C # เกี่ยวกับ structs คือปัญหาที่ใหญ่ที่สุดในหลาย ๆ ด้านเช่นกันภาษาให้สิ่งอำนวยความสะดวกที่ไม่เพียงพอสำหรับการระบุว่าการเปลี่ยนแปลงบางอย่างนั้นเหมาะสมหรือไม่เหมาะสมและการขาดสิ่งอำนวยความสะดวกดังกล่าว ตัวอย่างเช่น 99% ของ "การเปลี่ยนแปลงที่ไม่แน่นอนเป็นความชั่วร้าย" เกิดจากการเปลี่ยนMyListOfPoint[3].Offset(2,3);เป็นคอมไพเลอร์ของvar temp=MyListOfPoint[3]; temp.Offset(2,3);การแปลงที่ปลอมเมื่อใช้ ...
supercat

... กับOffsetวิธีการ วิธีที่เหมาะสมในการป้องกันโค้ดปลอมดังกล่าวไม่ควรทำให้ structs ไม่เปลี่ยนรูปแบบโดยไม่จำเป็น แต่ให้อนุญาตวิธีOffsetการที่จะติดแท็กด้วยคุณสมบัติที่ห้ามการแปลงดังกล่าว การแปลงเชิงตัวเลขโดยปริยายก็อาจจะดีกว่ามากหากสามารถติดแท็กได้เพื่อให้สามารถใช้งานได้เฉพาะในกรณีที่การเรียกใช้จะชัดเจน หากมีการโอเวอร์โหลดสำหรับfoo(float,float)และfoo(double,double)ฉันจะโพสต์ว่าพยายามใช้floatและdoubleมักจะไม่ควรใช้การแปลงโดยนัย แต่ควรเป็นข้อผิดพลาดแทน
supercat

การกำหนดค่าโดยตรงให้doubleกับ a floatหรือส่งผ่านไปยังวิธีการที่สามารถfloatโต้แย้งได้ แต่ไม่ใช่doubleจะทำสิ่งที่โปรแกรมเมอร์ตั้งใจจะทำอยู่เสมอ ในทางตรงกันข้ามการกำหนดfloatนิพจน์ให้doubleโดยไม่ต้องมี typecast ชัดเจนมักจะเป็นความผิดพลาด เวลาเดียวที่อนุญาตให้มีdouble->floatการแปลงโดยนัยจะทำให้เกิดปัญหาเมื่อถึงเวลาที่จะเลือกโอเวอร์โหลดที่เกินกว่าอุดมคติ ฉันจะวางตัวว่าวิธีที่ถูกต้องในการป้องกันไม่ให้มีการห้ามการใช้ double-> float แต่การติดแท็ก overloads ด้วยคุณสมบัติที่ไม่อนุญาตให้ทำการแปลง
supercat

8

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

คลาสและโครงสร้าง (คู่มือการเขียนโปรแกรม C #)


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

6

MYTH # 1: โครงสร้างเป็นคลาสน้ำหนักเบา

ตำนานนี้มีหลายรูปแบบ บางคนเชื่อว่าประเภทค่าไม่สามารถหรือไม่ควรมีวิธีการหรือพฤติกรรมที่สำคัญอื่น ๆ พวกเขาควรใช้เป็นประเภทการถ่ายโอนข้อมูลอย่างง่ายโดยมีเพียงฟิลด์สาธารณะหรือคุณสมบัติแบบง่าย ประเภท DateTime เป็นตัวอย่างที่ดีของสิ่งนี้: มันสมเหตุสมผลที่จะเป็นประเภทค่าในแง่ของการเป็นหน่วยพื้นฐานเช่นตัวเลขหรือตัวอักษรและมันก็สมเหตุสมผลที่จะทำการคำนวณตาม คุณค่าของมัน เมื่อมองสิ่งต่าง ๆ ประเภทการถ่ายโอนข้อมูลควรเป็นประเภทอ้างอิงต่อไป - การตัดสินใจควรขึ้นอยู่กับค่าที่ต้องการหรือความหมายประเภทอ้างอิงไม่ใช่ความเรียบง่ายของประเภท คนอื่น ๆ เชื่อว่าประเภทค่านั้น“ เบากว่า” มากกว่าประเภทการอ้างอิงในแง่ของประสิทธิภาพ ความจริงก็คือในบางกรณีประเภทของมูลค่ามีประสิทธิภาพมากกว่า - พวกเขาไม่ต้องการการเก็บขยะเว้นแต่จะถูกบรรจุไว้ในกล่องไม่มีค่าใช้จ่ายในการระบุประเภทและไม่จำเป็นต้องมีการยกเลิกการลงทะเบียน แต่ในทางอื่น ๆ ประเภทการอ้างอิงนั้นมีประสิทธิภาพมากกว่า - การผ่านพารามิเตอร์การกำหนดค่าให้กับตัวแปรการส่งคืนค่าและการดำเนินการที่คล้ายกันนั้นจะต้องมีการรวบรวม 4 หรือ 8 ไบต์เท่านั้น (ขึ้นอยู่กับว่าคุณกำลังใช้ CLR 32 บิตหรือ 64 บิต ) แทนที่จะคัดลอกข้อมูลทั้งหมด ลองคิดดูว่า ArrayList เป็นประเภทค่า "บริสุทธิ์" อย่างใดและส่งผ่านนิพจน์ ArrayList ไปยังวิธีการที่เกี่ยวข้องกับการคัดลอกข้อมูลทั้งหมด! ในเกือบทุกกรณีประสิทธิภาพไม่ได้พิจารณาจากการตัดสินใจประเภทนี้ คอขวดไม่เคยเป็นที่คุณคิดว่าจะเป็นและก่อนที่คุณจะตัดสินใจออกแบบตามประสิทธิภาพ คุณควรวัดตัวเลือกต่าง ๆ เป็นที่น่าสังเกตว่าการรวมกันของความเชื่อทั้งสองนั้นไม่ได้ผลเช่นกัน ไม่ว่าจะมีวิธีการพิมพ์จำนวนเท่าใด (ไม่ว่าจะเป็นคลาสหรือโครงสร้าง) - หน่วยความจำที่ใช้ต่ออินสแตนซ์จะไม่ได้รับผลกระทบ (มีค่าใช้จ่ายในแง่ของหน่วยความจำที่ใช้สำหรับโค้ดเอง แต่เกิดขึ้นหนึ่งครั้งแทนที่จะเป็นสำหรับแต่ละอินสแตนซ์)

MYTH # 2: ประเภทการอ้างอิงอยู่บนกอง ประเภทค่ามีชีวิตอยู่บนกอง

หนึ่งนี้มักเกิดจากความเกียจคร้านในส่วนของคนที่ทำซ้ำมัน ส่วนแรกถูกต้องตัวอย่างของประเภทการอ้างอิงจะถูกสร้างขึ้นบนฮีปเสมอ มันเป็นส่วนที่สองที่ทำให้เกิดปัญหา ตามที่ฉันได้สังเกตเห็นแล้วค่าของตัวแปรจะอยู่ที่ใดก็ตามที่มันประกาศดังนั้นถ้าคุณมีคลาสที่มีตัวแปรอินสแตนซ์ของ type int ค่าของตัวแปรนั้นสำหรับวัตถุที่กำหนดจะเป็นที่ที่ข้อมูลส่วนที่เหลือของวัตถุนั้นอยู่เสมอ บนกอง เฉพาะตัวแปรโลคัล (ตัวแปรที่ประกาศภายในเมธอด) และพารามิเตอร์เมธอดอยู่บนสแต็ก ใน C # 2 และใหม่กว่าแม้ตัวแปรท้องถิ่นบางตัวจะไม่ได้อยู่บนสแต็คอย่างที่คุณจะเห็นเมื่อเราดูวิธีการที่ไม่ระบุชื่อในบทที่ 5 แนวคิดเหล่านี้เกี่ยวข้องกันอย่างไรตอนนี้หรือไม่ เป็นที่ถกเถียงกันอยู่ว่าถ้าคุณกำลังเขียนโค้ดที่มีการจัดการคุณควรปล่อยให้ runtime กังวลเกี่ยวกับวิธีการใช้หน่วยความจำที่ดีที่สุด อันที่จริง ข้อกำหนดของภาษาไม่รับประกันว่าจะมีชีวิตอยู่ที่ไหน รันไทม์ในอนาคตอาจสามารถสร้างวัตถุบางอย่างบนสแต็กหากรู้ว่าสามารถหนีไปได้หรือคอมไพเลอร์ C # สามารถสร้างรหัสที่แทบจะไม่ใช้สแต็กเลย ตำนานต่อไปมักจะเป็นเพียงปัญหาคำศัพท์

MYTH # 3: วัตถุถูกส่งผ่านโดยอ้างอิงใน C # โดยเริ่มต้น

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

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

เมื่อวิธีนี้เรียกว่าค่าพารามิเตอร์ (การอ้างอิงถึง StringBuilder) จะถูกส่งผ่านโดยค่า หากคุณต้องเปลี่ยนค่าของตัวแปรตัวสร้างภายในเมธอด - ตัวอย่างเช่นด้วยตัวสร้างคำสั่ง = null; - ผู้โทรจะไม่เห็นการเปลี่ยนแปลงซึ่งตรงกันข้ามกับตำนาน เป็นที่น่าสนใจที่จะทราบว่าไม่เพียง แต่เป็นบิตของ "การอ้างอิง" ของตำนานที่ไม่ถูกต้อง แต่ยังเป็นบิต "ผ่านวัตถุ" วัตถุจะไม่ถูกส่งผ่านโดยการอ้างอิงหรือตามค่า เมื่อชนิดการอ้างอิงเกี่ยวข้องตัวแปรจะถูกส่งผ่านโดยการอ้างอิงหรือค่าของอาร์กิวเมนต์ (การอ้างอิง) ถูกส่งผ่านโดยค่า นอกเหนือจากสิ่งอื่นแล้วสิ่งนี้ยังตอบคำถามว่าเกิดอะไรขึ้นเมื่อมีการใช้ค่า null เป็นอาร์กิวเมนต์แบบค่า - ถ้าวัตถุถูกส่งไปรอบ ๆ นั่นจะทำให้เกิดปัญหาเนื่องจากไม่มีวัตถุที่จะผ่าน! แทน, การอ้างอิงแบบ null จะถูกส่งผ่านโดยค่าในลักษณะเดียวกับการอ้างอิงอื่นใด หากคำอธิบายอย่างรวดเร็วนี้ทำให้คุณสับสนคุณอาจต้องการดูบทความของฉัน“ พารามิเตอร์ผ่านใน C #,” (http://mng.bz/otVt ) ซึ่งจะมีรายละเอียดมากขึ้น ความเชื่อผิด ๆ เหล่านี้ไม่ใช่สิ่งเดียว การชกมวยและการไม่ทำกล่องเข้ามาเพื่อแบ่งปันความเข้าใจผิดอย่างยุติธรรมซึ่งฉันจะพยายามทำให้ชัดเจนขึ้นในครั้งต่อไป

การอ้างอิง: C # ในความลึกครั้งที่ 3 โดย Jon Skeet


1
ดีมากสมมติว่าคุณถูกต้อง ยังดีมากที่จะเพิ่มการอ้างอิง
NoChance

5

ฉันคิดว่าการประมาณแรกที่ดีคือ "ไม่เคย"

ฉันคิดว่าการประมาณที่ดีครั้งที่สองคือ "ไม่เคย"

หากคุณต้องการความสมบูรณ์ให้ลองพิจารณาดู แต่ก็ควรทำการวัดเสมอ


24
ฉันไม่เห็นด้วยกับคำตอบนั้น โครงสร้างมีการใช้งานอย่างถูกกฎหมายในหลาย ๆ สถานการณ์ นี่คือตัวอย่าง - การจัดการข้ามข้อมูลในลักษณะปรมาณู
Franci Penov

25
คุณควรแก้ไขโพสต์ของคุณและทำอย่างละเอียดในประเด็นของคุณ - คุณได้ให้ความเห็นของคุณ แต่คุณควรสำรองข้อมูลด้วยเหตุผลที่คุณรับความคิดเห็นนี้
Erik Forbes

4
ฉันคิดว่าพวกเขาต้องการการ์ด Totin 'Chip เทียบเท่า ( en.wikipedia.org/wiki/Totin%27_Chip ) เพื่อใช้งาน structs อย่างจริงจัง.
Greg

4
คน 87.5K โพสต์คำตอบเช่นนี้ได้อย่างไร เขาทำมันในขณะที่เขายังเป็นเด็กหรือไม่?
Rohit Vipin Mathews

3
@Rohit - เมื่อหกปีก่อน มาตรฐานเว็บไซต์แตกต่างกันมากแล้ว นี่ยังเป็นคำตอบที่ไม่ดี แต่คุณพูดถูก
Andrew Arnold

5

ผมก็แค่จัดการกับ Windows Communication Foundation [WCF] ชื่อท่อและฉันไม่ได้แจ้งให้ทราบว่ามันจะทำให้ความรู้สึกที่จะใช้ Structs ในการสั่งซื้อเพื่อให้แน่ใจว่าการแลกเปลี่ยนข้อมูลที่เป็นประเภทค่าแทนชนิดการอ้างอิง


1
นี่คือเงื่อนงำที่ดีที่สุดของทั้งหมด IMHO
Ivan

4

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

ดังนั้นคุณควรใช้ structs เมื่อคุณต้องการแสดงโครงสร้างข้อมูลที่ง่ายขึ้นและโดยเฉพาะอย่างยิ่งถ้าคุณรู้ว่าคุณจะสร้างโครงสร้างข้อมูลจำนวนมากได้ทันที มีตัวอย่างจำนวนมากใน. NET Framework ซึ่ง Microsoft ใช้โครงสร้างแทนคลาสต่างๆเช่นจุดสี่เหลี่ยมผืนผ้าและโครงสร้างสี


3

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

http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/


3

โครงสร้างหรือประเภทค่าสามารถใช้ในสถานการณ์ต่อไปนี้ -

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

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


3

สั้น ๆ ใช้ struct ถ้า:

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

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

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


2

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

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


-11

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


2
ไม่สามารถสืบทอดโครงสร้างได้ให้ดูmsdn.microsoft.com/en-us/library/0taef578.aspx
HimBromBeere
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.