ตัวดำเนินการ 'with' ของ C# Records สร้างข้อมูลที่ไม่สอดคล้องกันเมื่อใช้งานร่วมกับ Computed Properties

ทีมชุมชน BigGo
ตัวดำเนินการ 'with' ของ C# Records สร้างข้อมูลที่ไม่สอดคล้องกันเมื่อใช้งานร่วมกับ Computed Properties

ปัญหาสำคัญของ C# records ได้เกิดขึ้นและทำให้แม้แต่นักพัฒนาที่มีประสบการณ์ยังต้องประหลาดใจ ปัญหานี้เกิดขึ้นเมื่อใช้ตัวดำเนินการ with ร่วมกับ computed properties ซึ่งนำไปสู่ความไม่สอดคล้องของข้อมูลที่อาจยากต่อการแก้ไขข้อบกพร่อง

ปัญหาหลักของ Nondestructive Mutation

C# records ได้นำเสนอตัวดำเนินการ with สำหรับ nondestructive mutation ที่ช่วยให้นักพัฒนาสามารถสร้าง instance ใหม่ที่มีการปรับเปลี่ยนค่า property ได้ อย่างไรก็ตาม ตัวดำเนินการนี้ไม่ทำงานตามที่นักพัฒนาหลายคนคาดหวัง แทนที่จะเรียก constructor ด้วยค่าใหม่ มันจะทำการคัดลอกหน่วยความจำของ record เดิมแล้วตั้งค่า field ที่ระบุโดยตรง วิธีการนี้จะข้าม property calculations ที่เกิดขึ้นในช่วงการเริ่มต้น

เมื่อ record มี computed properties ที่ได้รับค่าจาก field อื่นๆ ในระหว่างการเริ่มต้น การใช้ with จะสร้าง object ที่ไม่สอดคล้องกัน ตัวอย่างเช่น record ที่คำนวณว่าตัวเลขเป็นเลขคู่หรือเลขคี่ในระหว่างการสร้าง จะยังคงเก็บค่าที่คำนวณเก่าไว้แม้ว่าตัวเลขพื้นฐานจะเปลี่ยนแปลงผ่านการดำเนินการ with

Computed properties: Properties ที่คำนวณค่าของตนเองจากข้อมูลอื่นๆ ในระหว่างการสร้าง object แทนที่จะถูกตั้งค่าโดยตรง

โค้ดตัวอย่างแสดงปัญหา:

public sealed record Number(int Value)
{
    public bool Even { get; } = (Value & 1) == 0;
}

var n2 = new Number(2);           // Even = True (ถูกต้อง)
var n3 = n2 with { Value = 3 };   // Even = True (ผิด ควรจะเป็น False)

การถกเถียงในชุมชนเกี่ยวกับพฤติกรรมที่คาดหวัง

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

การอภิปรายได้เผยให้เห็นว่าปัญหานี้ส่งผลกระทบต่อนักพัฒนาหลายคนที่ค้นพบมันโดยอิสระ ซึ่งบ่งชี้ว่าเป็นปัญหาการใช้งานที่แท้จริงมากกว่าเป็นกรณีพิเศษ

ตัวเลือก Workaround ที่จำกัด

นักพัฒนาที่เผชิญกับปัญหานี้มีทางแก้ไขที่น่าพอใจเพียงไม่กี่วิธี วิธีที่ตรงไปตรงมาที่สุดคือการหลีกเลี่ยงการดำเนินการ with บน records ที่มี computed properties ทางแก้ไขอื่นๆ รวมถึงการเขียน custom Roslyn analyzers เพื่อตรวจจับรูปแบบการใช้งานที่มีปัญหาหรือการใช้ lazy initialization schemes ที่ซับซ้อน

อย่างไรก็ตาม workarounds เหล่านี้เพิ่มความซับซ้อนและภาระการบำรุงรักษาอย่างมาก วิธี lazy initialization ต้องการการจัดการ computed values ด้วยตนเอง ในขณะที่โซลูชันที่ใช้ analyzer ทำงานได้เฉพาะในสภาพแวดล้อมการพัฒนาเฉพาะเท่านั้น

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

ตัวเลือกการแก้ไขปัญหาชั่วคราวที่มีอยู่:

  • ตัวเลือกที่ 1: หลีกเลี่ยงการใช้ตัวดำเนินการ with สำหรับ records ที่มี computed properties
  • ตัวเลือกที่ 2: เขียน Roslyn analyzer เพื่อตรวจจับรูปแบบการใช้งานที่มีปัญหา
  • ตัวเลือกที่ 3: ใช้งาน lazy initialization ด้วยคลาส wrapper Lazy<T>
  • ตัวเลือกที่ 4: ขอให้มีการเปลี่ยนแปลงข้อกำหนดของภาษา (วิธีแก้ไขระยะยาว)

การแลกเปลี่ยนระหว่างประสิทธิภาพและความถูกต้อง

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

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

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

สถานการณ์นี้เป็นเครื่องเตือนใจว่าแม้แต่ feature ของภาษาที่มีชื่อเสียงก็สามารถมีข้อผิดพลาดที่ละเอียดอ่อนซึ่งส่งผลกระทบต่อแอปพลิเคชันในโลกจริง นักพัฒนาที่ทำงานกับ C# records ควรพิจารณาอย่างรอบคอบว่ากรณีการใช้งานของพวกเขาเกี่ยวข้องกับ computed properties หรือไม่ก่อนที่จะพึ่งพาตัวดำเนินการ with อย่างมาก

อ้างอิง: UNEXPECTED INCONSISTENCY IN RECORDS