ปฏิบัติที่ดีที่สุด

รหัสที่เป็นมิตรกับคอมไพเลอร์: คำหลักที่ปิดผนึกใน .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 คุณไม่ต้องจ่ายเพิ่มใดๆ
▶ สมัครรับ จดหมายข่าวของฉัน เพื่อรับแนวทางปฏิบัติที่ดีที่สุด บทช่วยสอน คำแนะนำ เคล็ดลับ และสิ่งดีๆ อื่น ๆ อีกมากมายส่งตรงถึงกล่องจดหมายของคุณ

แหล่งข้อมูลอื่นๆ

นี่เป็นแหล่งข้อมูลอื่นๆ ที่คุณอาจพบว่ามีประโยชน์