ปฏิบัติที่ดีที่สุด
รหัสที่เป็นมิตรกับคอมไพเลอร์: คำหลักที่ปิดผนึกใน .NET C#
เหตุใดและเมื่อปิดผนึกคำหลักอาจนำไปสู่การเพิ่มประสิทธิภาพใน .NET C#
การเขียนโค้ดที่เป็นมิตรกับคอมไพเลอร์หมายความว่าอย่างไร
รหัส .NET ใดๆ ผ่านมากกว่าหนึ่งเฟสจนกระทั่งถึงรหัสเครื่องในที่สุด เนื่องจากมีปัจจัยหลายอย่างที่เกี่ยวข้องกับกระบวนการนี้ จึงอาจมีรายละเอียดมากมายที่เราพลาดไปเมื่อเราเขียนโค้ดครั้งแรก
อย่างไรก็ตาม ยิ่งโค้ดที่เราเขียนชัดเจนและกำหนดได้ชัดเจนยิ่งขึ้น คอมไพเลอร์ก็จะสามารถช่วยเราและสร้างโค้ดเครื่องที่ปรับให้เหมาะสมได้มากขึ้นเท่านั้น
ในบทความนี้ เราจะพูดถึงตัวอย่างหนึ่งของวิธีที่เราสามารถช่วยให้คอมไพเลอร์เพิ่มประสิทธิภาพโค้ดของเราได้ วิธีนี้คือ; โดยใช้ คำหลักที่ปิดผนึก
พูดจบแล้วมาดูตัวอย่างกัน...
ตรวจสอบประวัติ
หากคุณเป็นนักพัฒนา .NET แม้แต่มือใหม่ คุณควรรู้แล้วว่าตอนนี้มีคำหลักในกรอบงาน .NET ที่เรียกว่า sealed
คำหลักนี้สามารถใช้ใน คำจำกัดความของคลาส ซึ่งหมายความว่าคลาสนี้ไม่สามารถสืบทอดโดยคลาสอื่นได้ ดูเหมือนว่านี้:
public sealed class MyClass {}
หรือแม้แต่ในการประกาศเมธอด นั่นหมายความว่าเมธอดนั้นไม่สามารถถูกแทนที่อีกต่อไปด้วยเมธอดอื่นในคลาสย่อย กล่าวอีกนัยหนึ่ง มันจะทำลายชุดการแทนที่วิธีการในระดับที่มีการใช้งาน ดูเหมือนว่านี้:
public sealed override void MyMethod() {}
ดังนั้น สิ่งที่เราเข้าใจได้จากสิ่งนี้ก็คือ เมื่อเราใช้คีย์เวิร์ด sealed
เราสัญญากับคอมไพเลอร์ว่าเราไม่มีความตั้งใจที่จะสืบทอดจากคลาสหรือแทนที่เมธอด
ต้องบอกว่าตอนนี้เรามาดูกันว่ามันจะมีความหมายอะไรกับคอมไพเลอร์หรือไม่
เริ่มต้นด้วยโค้ดพื้นฐานบางส่วนเพื่อใช้ตลอดทั้งคำอธิบาย
เบสคลาส
public class BaseClass { public virtual void DoSomething() { } public void DoSomethingElse() { } }
นี่คือคลาสพื้นฐานที่เราจะใช้เป็นพาเรนต์บนสุด
ในชั้นเรียนนี้ เรามีสมาชิกที่กำหนดไว้ดังต่อไปนี้:
public virtual void DoSomething()
วิธีการpublic void DoSomethingElse()
วิธีการ
ห้องเรียนของฉัน
public class MyClass : BaseClass { public override void DoSomething() { } }
นี่คือคลาสที่จะสืบทอดมาจาก BaseClass
แต่โดยไม่ต้องใช้คีย์เวิร์ด sealed
ในคำจำกัดความ
ในคลาสนี้ เรากำลังแทนที่เมธอด DoSomething
ซึ่งสืบทอดมาจากคลาสพาเรนต์ BaseClass
MySealedClass
public sealed class MySealedClass : BaseClass { public override void DoSomething() { } }
นี่คือคลาสที่จะสืบทอดมาจาก BaseClass
แต่คราวนี้เราใช้คีย์เวิร์ด sealed
ในคำจำกัดความ
ในคลาสนี้ เรากำลังแทนที่เมธอด DoSomething
ซึ่งสืบทอดมาจากคลาสพาเรนต์ BaseClass
ตอนนี้ เรามาดำเนินการต่อและดูว่าจะมีความแตกต่างหรือไม่ - จากมุมมองของคอมไพเลอร์ - ระหว่างการใช้คลาส MyClass
และ MySealedClass
การเรียกวิธีการเสมือน
เพื่อตรวจสอบว่าจะมีความแตกต่างใดๆ จากมุมมองของคอมไพลเลอร์ระหว่าง การเรียกใช้เมธอดเสมือน ในทั้งคลาส MyClass
และ MySealedClass
หรือไม่ เราจะสร้างโปรเจ็กต์ เกณฑ์มาตรฐาน
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); [Benchmark] public void CallingVirtualMethodOnMyClass() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObject.DoSomething(); } } [Benchmark] public void CallingVirtualMethodOnMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObject.DoSomething(); } } }
ตอนนี้ เมื่อรันโปรเจ็กต์ Benchmark นี้ เราจะได้ผลลัพธ์ดังต่อไปนี้
ดังที่เราสังเกตเห็นได้ในที่นี้ ประสิทธิภาพของการเรียกเมธอดเสมือนบนคลาสที่ปิดผนึกนั้นดีกว่าการเรียกใช้บนคลาสที่ไม่มีการปิดผนึกมาก
แต่ทำไม!!! ให้ฉันบอกคุณ.
ในคลาสที่ไม่ปิดผนึก
ขณะเรียกใช้เมธอดเสมือนบนอ็อบเจ็กต์ที่สร้างจากคลาส MyClass
ในขณะนั้นคอมไพเลอร์ไม่รู้ว่ามีโค้ดบางตัวที่เตรียมใช้งานอ็อบเจ็กต์ _myClassObject
ด้วยอินสแตนซ์ใหม่ของคลาสลูกของคลาส MyClass
หรือไม่ สมมติฐานนี้ถูกต้องเนื่องจากคลาส MyClass
ไม่ได้ถูกปิดผนึก และนั่นหมายความว่าคลาสนั้นสามารถสืบทอดได้
ตามสมมติฐานนั้น คอมไพลเลอร์ไม่สามารถตัดสินใจได้ - ณ เวลาคอมไพล์ - ว่าการใช้งานจริงของเมธอด DoSomething
จะเป็นอันที่คลาส MyClass
หรือคลาสลูกอื่น ๆ จัดให้หรือไม่
ดังนั้น คอมไพเลอร์จะเขียนคำสั่งบางอย่าง -ที่จะดำเนินการขณะรันไทม์- เพื่อตรวจสอบในขณะที่ดำเนินการเมธอด DoSomething
ซึ่งการใช้งานจะเป็นคำสั่งที่ถูกต้อง การทำเช่นนี้จะทำให้ต้นทุนการประมวลผลและเวลามากขึ้นอย่างแน่นอน
หมายเหตุ: ตามที่คุณสังเกตเห็นว่าคอมไพเลอร์อาจสงสัยว่าโค้ดบางตัวสามารถเริ่มต้นวัตถุใหม่ได้ คุณอาจคิดว่าการทำเครื่องหมายฟิลด์เป็น readonly
จะช่วยแก้ปัญหาได้ แต่จริงๆ แล้วยังไม่สามารถเริ่มต้นวัตถุใหม่ได้ภายในตัวสร้าง
ในคลาสที่ปิดผนึก
ในขณะที่เรียกใช้เมธอดเสมือนบนออบเจ็กต์ที่สร้างจากคลาส MySealedClass
ในขณะนั้นคอมไพเลอร์ไม่ทราบว่ามีโค้ดบางตัวที่เริ่มต้นออบเจ็กต์ _mySealedClassObject
ด้วยอินสแตนซ์ใหม่หรือไม่ อย่างไรก็ตาม คอมไพเลอร์มั่นใจว่าแม้ว่าสิ่งนี้จะเกิดขึ้น อินสแตนซ์ก็จะยังคงเป็นคลาส MySealedClass
เหมือนที่เป็น sealed
และนั่นหมายความว่าจะไม่มีคลาสย่อยเลย
จากนั้น คอมไพลเลอร์จะตัดสินใจ - ณ เวลาคอมไพล์ - การใช้งานจริงของเมธอด DoSomething
การดำเนินการนี้เร็วกว่าการรอรันไทม์มากอย่างแน่นอน
การเรียกวิธีการที่ไม่ใช่เสมือน
เพื่อตรวจสอบว่าจะมีความแตกต่างหรือไม่ - จากมุมมองของคอมไพลเลอร์ - ระหว่าง การเรียกใช้เมธอดที่ไม่ใช่เสมือน ในทั้งคลาส MyClass
และ MySealedClass
เราจะสร้างโปรเจ็กต์ Benchmark .
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); private MyClass[] _myClassObjectsArray = new MyClass[1]; private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1]; [Benchmark] public void CallingNonVirtualMethodOnMyClass() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObject.DoSomethingElse(); } } [Benchmark] public void CallingNonVirtualMethodOnMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObject.DoSomethingElse(); } } }
ตอนนี้ เมื่อรันโปรเจ็กต์ Benchmark นี้ เราจะได้ผลลัพธ์ดังต่อไปนี้
ดังที่เราสังเกตเห็นได้ในที่นี้ ประสิทธิภาพของการเรียกเมธอดที่ไม่ใช่เสมือนบนคลาสที่ปิดผนึกนั้นดีกว่าการเรียกใช้บนคลาสที่ไม่มีการปิดผนึก
อย่างไรก็ตาม ไม่มีหลักฐานทางวิทยาศาสตร์ว่าทำไมสิ่งนี้จึงเกิดขึ้น และการดำเนินโครงการมาตรฐานเดียวกันอีกครั้งจริง ๆ อาจได้รับผลลัพธ์ที่ตรงกันข้าม
ดังนั้นความแตกต่างนี้ส่วนใหญ่น่าจะเกิดจากกรอบการเปรียบเทียบเนื่องจากความแตกต่างนั้นน้อยเกินไปจนสามารถมองข้ามได้
การตรวจสอบประเภท
เพื่อตรวจสอบว่าจะมีความแตกต่างใดๆ หรือไม่ -จากมุมมองของคอมไพลเลอร์- ระหว่าง การตรวจสอบประเภทของออบเจ็กต์โดยใช้ตัวดำเนินการ is
ในทั้งคลาส MyClass
และ MySealedClass
เราจะสร้าง เกณฑ์มาตรฐาน< / แข็งแกร่ง> โครงการ
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); [Benchmark] public bool ObjectTypeIsMyClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject is MyClass; } return true; } [Benchmark] public bool ObjectTypeIsMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject is MySealedClass; } return true; } }
ตอนนี้ เมื่อรันโปรเจ็กต์ Benchmark นี้ เราจะได้ผลลัพธ์ดังต่อไปนี้
ดังที่เราสังเกตเห็นได้ในที่นี้ ประสิทธิภาพในการตรวจสอบประเภทอ็อบเจ็กต์บนคลาสที่ปิดผนึกนั้นดีกว่าการเรียกมันในคลาสที่ไม่ปิดผนึก
แต่ทำไม!!! ให้ฉันบอกคุณ.
ในคลาสที่ไม่ปิดผนึก
ในขณะที่ตรวจสอบว่าประเภทของอ็อบเจ็กต์เป็นคลาส MyClass
หรือไม่ คอมไพลเลอร์จำเป็นต้องตรวจสอบว่าอ็อบเจ็กต์นั้นเป็นคลาสประเภท MyClass
หรือคลาสลูกใด ๆ
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำเพิ่มเติมและการประมวลผลและเวลาที่เพิ่มขึ้นอย่างแน่นอน
ในคลาสที่ปิดผนึก
ในขณะที่ตรวจสอบว่าประเภทของอ็อบเจ็กต์เป็นคลาส MySealedClass
หรือไม่ คอมไพเลอร์จำเป็นต้องตรวจสอบว่าอ็อบเจ็กต์นั้นอยู่ในคลาสประเภท MySealedClass
เท่านั้นหรือไม่ ไม่มีอะไรอื่นอีก เนื่องจากคลาส MySealedClass
ถูกผนึกไว้ และนั่นหมายความว่าจะไม่มีคลาสลูกใดๆ เลย
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำน้อยลงและการประมวลผลและเวลาน้อยลงอย่างแน่นอน
ประเภทการหล่อ
เพื่อตรวจสอบว่าจะมีความแตกต่างหรือไม่ -จากมุมมองของคอมไพลเลอร์- ระหว่าง การส่งออบเจ็กต์โดยใช้ตัวดำเนินการ as
ในทั้งคลาส MyClass
และ MySealedClass
เราจะสร้าง เกณฑ์มาตรฐาน โครงการ.
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); [Benchmark] public void ObjectTypeAsMyClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject as MyClass; } } [Benchmark] public void ObjectTypeAsMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject as MySealedClass; } } }
ตอนนี้ เมื่อรันโปรเจ็กต์ Benchmark นี้ เราจะได้ผลลัพธ์ดังต่อไปนี้
ดังที่เราสังเกตเห็นได้ในที่นี้ ประสิทธิภาพของการหล่ออ็อบเจ็กต์บนคลาสที่ปิดผนึกนั้นดีกว่าการเรียกมันบนคลาสที่ไม่มีการปิดผนึก
แต่ทำไม!!! ให้ฉันบอกคุณ.
ในคลาสที่ไม่ปิดผนึก
ในขณะที่ส่งอ็อบเจ็กต์เป็นคลาส MyClass
คอมไพลเลอร์จำเป็นต้องตรวจสอบว่าอ็อบเจ็กต์นั้นเป็นคลาสประเภท MyClass
หรือคลาสลูกใด ๆ
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำเพิ่มเติมและการประมวลผลและเวลาที่เพิ่มขึ้นอย่างแน่นอน
ในคลาสที่ปิดผนึก
ในขณะที่ส่งอ็อบเจ็กต์เป็นคลาส MySealedClass
คอมไพเลอร์จำเป็นต้องตรวจสอบว่าอ็อบเจ็กต์นั้นเป็นคลาสประเภท MySealedClass
เท่านั้นหรือไม่ ไม่มีอะไรอื่นอีก เนื่องจากคลาส MySealedClass
ถูกผนึกไว้ และนั่นหมายความว่าจะไม่มีคลาสลูกใดๆ เลย
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำน้อยลงและการประมวลผลและเวลาน้อยลงอย่างแน่นอน
การจัดเก็บวัตถุในอาร์เรย์
เพื่อตรวจสอบว่าจะมีความแตกต่างหรือไม่ -จากมุมมองของคอมไพลเลอร์- ระหว่าง การจัดเก็บวัตถุในอาร์เรย์ในทั้งคลาส MyClass
และ MySealedClass
เราจะสร้างโปรเจ็กต์ Benchmark .
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); private MyClass[] _myClassObjectsArray = new MyClass[1]; private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1]; [Benchmark] public void StoringValuesInMyClassArray() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObjectsArray[0] = _myClassObject; } } [Benchmark] public void StoringValuesInMySealedClassArray() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObjectsArray[0] = _mySealedClassObject; } } }
ตอนนี้ เมื่อรันโปรเจ็กต์ Benchmark นี้ เราจะได้ผลลัพธ์ดังต่อไปนี้
ดังที่เราเห็นได้ในที่นี้ ประสิทธิภาพในการจัดเก็บอ็อบเจ็กต์ในอาร์เรย์บนคลาสที่ปิดผนึกนั้นดีกว่าการเรียกมันในคลาสที่ไม่ปิดผนึก
แต่ทำไม!!! ให้ฉันบอกคุณ.
ก่อนที่จะลงรายละเอียด ผมขอเตือนคุณถึงประเด็นสำคัญก่อน อาร์เรย์เป็นตัวแปรร่วม
ซึ่งหมายความว่าหากเรากำหนดคลาสต่อไปนี้:
public class A {} public class B : A {}
จากนั้นรหัสต่อไปนี้จะถูกต้อง:
A[] arrayOfA = new B[5];
นอกจากนี้ เราสามารถตั้งค่ารายการภายใน arrayOfA
ให้เป็นอินสแตนซ์ B
ได้ดังนี้:
arrayOfA[0] = new B();
ต้องบอกว่าตอนนี้เรามาดูหัวข้อหลักของเรากันดีกว่า
ในคลาสที่ไม่ปิดผนึก
ขณะตั้งค่ารายการภายในอาร์เรย์ _myClassObjectsArray
คอมไพลเลอร์จำเป็นต้องตรวจสอบว่าอินสแตนซ์ _myClassObject
ที่เราใช้อยู่นั้นเป็นคลาสประเภท MyClass
หรือคลาสย่อยใด ๆ
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำเพิ่มเติมและการประมวลผลและเวลาที่เพิ่มขึ้นอย่างแน่นอน
ในคลาสที่ปิดผนึก
ในขณะที่ตั้งค่ารายการภายในอาร์เรย์ _mySealedClassObjectsArray
คอมไพเลอร์จำเป็นต้องตรวจสอบว่าอินสแตนซ์ _mySealedClassObject
ที่เราใช้อยู่นั้นเป็นคลาสประเภท MySealedClass
เท่านั้น ไม่มีอะไรอื่นอีก เนื่องจากคลาส MySealedClass
ถูกผนึกไว้ และนั่นหมายความว่าจะไม่มีคลาสลูกใดๆ เลย
ดังนั้นสิ่งนี้จึงนำไปสู่คำแนะนำน้อยลงและการประมวลผลและเวลาน้อยลงอย่างแน่นอน
การตรวจจับความล้มเหลวตั้งแต่เนิ่นๆ
นอกจากประสิทธิภาพที่เพิ่มขึ้นที่เราจะได้รับจากการใช้คำสำคัญ sealed
แล้ว เรายังสามารถหลีกเลี่ยงความล้มเหลวรันไทม์บางอย่างได้อีกด้วย ฉันขอแสดงตัวอย่างให้คุณดู
ถ้าเราเขียนโค้ดต่อไปนี้:
public void Run(MyClass obj) { _ = _baseClassObject as IMyInterface; }
คอมไพเลอร์ - ณ เวลาออกแบบ - จะไม่แสดงคำเตือนหรือข้อผิดพลาดใดๆ เนื่องจากจริงๆ แล้ว obj
อาจเป็นคลาสประเภท MyClass
หรือคลาสลูกใดๆ ก็ได้ ดังนั้นคอมไพลเลอร์จึงจำเป็นต้องรอให้รันไทม์ทำการตรวจสอบขั้นสุดท้าย
แน่นอนว่าหากขณะรันไทม์ประเภทจริงของ obj
กลับกลายเป็นว่าไม่ได้ใช้ IMyInterface
สิ่งนี้จะทำให้เกิดข้อยกเว้นรันไทม์
อย่างไรก็ตาม ถ้าเราเขียนโค้ดต่อไปนี้:
public void Run(MySealedClass obj) { _ = _baseClassObject as IMyInterface; }
คอมไพลเลอร์จะแสดงข้อผิดพลาด (CS0039) ในเวลาออกแบบ เนื่องจาก obj
อาจเป็นคลาสประเภท MySealedClass
เท่านั้น ไม่มีอย่างอื่นอีก ดังนั้นคอมไพลเลอร์สามารถตรวจสอบได้ทันทีว่าคลาส MySealedClass
ใช้งาน IMyInterface
หรือไม่
ดังนั้น นี่หมายความว่าการใช้คีย์เวิร์ดที่ปิดผนึกทำให้คอมไพเลอร์สามารถดำเนินการเปลี่ยนแปลงคงที่ที่เหมาะสมในขณะออกแบบได้
ความคิดสุดท้าย
ฉันขอแนะนำให้ใช้คำสำคัญที่ปิดสนิททุกครั้งที่ทำได้
นี่ไม่เพียงแต่เพื่อประสิทธิภาพที่เพิ่มขึ้นที่คุณอาจได้รับเท่านั้น แต่ยังเป็นเพราะเป็นแนวทางปฏิบัติที่ดีที่สุดจากมุมมองของการออกแบบจนถึงขอบเขตที่ Microsoft กำลังคิดที่จะทำให้คลาสทั้งหมดถูกปิดผนึกโดยค่าเริ่มต้น
สุดท้ายนี้ ฉันหวังว่าคุณจะสนุกกับการอ่านบทความนี้ในขณะที่ฉันสนุกกับการเขียนมัน
หวังว่าเนื้อหานี้จะเป็นประโยชน์ หากคุณต้องการสนับสนุน:
▶ หากคุณยังไม่ได้เป็นสมาชิก Medium คุณสามารถใช้ ลิงก์การแนะนำของฉัน เพื่อที่ฉันจะได้รับค่าธรรมเนียมส่วนหนึ่งจาก Medium คุณไม่ต้องจ่ายเพิ่มใดๆ
▶ สมัครรับ จดหมายข่าวของฉัน เพื่อรับแนวทางปฏิบัติที่ดีที่สุด บทช่วยสอน คำแนะนำ เคล็ดลับ และสิ่งดีๆ อื่น ๆ อีกมากมายส่งตรงถึงกล่องจดหมายของคุณ
แหล่งข้อมูลอื่นๆ
นี่เป็นแหล่งข้อมูลอื่นๆ ที่คุณอาจพบว่ามีประโยชน์