struct ด้วยค่าเริ่มต้นที่ไร้สาระ


12

ในระบบของฉันฉันมักทำงานกับรหัสสนามบิน ( "YYZ", "LAX", "SFO"ฯลฯ ) พวกเขามักจะอยู่ในรูปแบบเดียวกันแน่นอน (3 ตัวอักษรแสดงเป็นตัวพิมพ์ใหญ่) โดยทั่วไประบบจะจัดการกับ 25-50 ของรหัส (ต่างกัน) ต่อคำขอ API ด้วยการจัดสรรรวมกว่าพันรายการระบบจะส่งผ่านแอปพลิเคชันของเราหลายเลเยอร์และเปรียบเทียบกับความเท่าเทียมกันบ่อยครั้ง

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

จากนี้ฉันตัดสินใจที่จะหยุดการส่งผ่านสายไปรอบ ๆ และสร้างAirportชั้นเรียนซึ่งมีตัวสร้างเดียวที่ใช้และตรวจสอบรหัสสนามบิน

public sealed class Airport
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0]) 
        || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.", 
                nameof(code));
        }

        Code = code.ToUpperInvariant();
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code;
    }

    private bool Equals(Airport other)
    {
        return string.Equals(Code, other.Code);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code?.GetHashCode() ?? 0;
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

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

อย่างไรก็ตามสิ่งที่ฉันสังเกตเห็นคือการรวบรวมขยะกำลังทำงานอยู่บ่อยครั้งมากขึ้นซึ่งฉันติดตามไปที่Airportการรวบรวมจำนวนมาก

วิธีการแก้ปัญหาของฉันไปนี้คือการแปลงเป็นclass structส่วนใหญ่เป็นเพียงการเปลี่ยนแปลงคำหลักยกเว้นGetHashCodeและToString:

public override string ToString()
{
    return Code ?? string.Empty;
}

public override int GetHashCode()
{
    return Code?.GetHashCode() ?? 0;
}

เพื่อจัดการกรณีที่default(Airport)มีการใช้

คำถามของฉัน:

  1. การสร้างAirportคลาสหรือโครงสร้างเป็นคำตอบที่ดีโดยทั่วไปหรือฉันกำลังแก้ไขปัญหาที่ผิด / แก้ไขด้วยวิธีที่ไม่ถูกต้องด้วยการสร้างประเภทหรือไม่ หากไม่ใช่วิธีที่ดีวิธีแก้ปัญหาที่ดีกว่าคืออะไร

  2. แอปพลิเคชันของฉันควรจัดการกับอินสแตนซ์ที่default(Airport)ใช้งานอย่างไร ประเภทของdefault(Airport)แอพพลิเคชั่นของฉันไร้สาระดังนั้นฉันจึงทำif (airport == default(Airport) { throw ... }ในสถานที่ซึ่งการได้รับอินสแตนซ์ของAirport(และCodeคุณสมบัติของมัน) มีความสำคัญต่อการดำเนินการ

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


7
การรวบรวมขยะมีผลกระทบอย่างมีนัยสำคัญต่อวิธีการใช้งานของคุณหรือไม่? ในคำอื่น ๆ มันมีความสำคัญ?
Robert Harvey

อย่างไรก็ตามการแก้ปัญหาในชั้นเรียนเป็นเรื่องที่ "ดี" วิธีที่คุณรู้ว่ามันแก้ไขปัญหาของคุณได้โดยไม่ต้องสร้างอะไรใหม่
Robert Harvey

2
วิธีหนึ่งที่คุณสามารถแก้ไขdefault(Airport)ปัญหาได้ก็คือเพียงแค่ไม่อนุญาตให้ใช้อินสแตนซ์เริ่มต้น คุณสามารถทำได้โดยการเขียนคอนสตรัคเตอร์แบบไร้พารามิเตอร์แล้วโยนInvalidOperationExceptionหรือNotImplementedExceptionลงไป
Robert Harvey

3
ในบันทึกด้านข้างแทนที่จะยืนยันว่าสตริงการเริ่มต้นของคุณมีอักขระ 3 ตัวจริงทำไมไม่เปรียบเทียบกับรายการ จำกัด ของรหัสสนามบินทั้งหมด (เช่นgithub.com/datasets/airport-codesหรือคล้ายกัน)
Dan Pichelman

2
ฉันยินดีที่จะเดิมพันหลายเบียร์ว่านี่ไม่ใช่รากของปัญหาด้านประสิทธิภาพ แล็ปท็อปปกติสามารถจัดสรรตามวัตถุ 10M สั่ง / วินาที
Esben Skov Pedersen

คำตอบ:


6

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


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

อย่างไรก็ตามหากคุณกำลังนำข้อมูลเข้าสู่ระบบจากแหล่งที่คุณไม่ได้ควบคุมคุณไม่จำเป็นต้องมีปัญหากับเธรดทั้งหมด ในกรณีนี้ struct เหมาะอย่างยิ่งสำหรับความจริงง่ายๆที่default(Airport)จะให้สิ่งอื่นนอกเหนือจากnullตัวชี้ สร้างค่าที่ชัดเจนเพื่อแสดงถึง "ไม่มีค่า" หรือ "ค่าเริ่มต้น" ดังนั้นคุณจึงมีบางอย่างที่จะพิมพ์บนหน้าจอหรือในไฟล์บันทึก (เช่น "---" เป็นต้น) ในความเป็นจริงฉันจะรักษาความเป็นcodeส่วนตัวและไม่เปิดเผยCodeทรัพย์สินเลยเพียงแค่มุ่งเน้นไปที่พฤติกรรมที่นี่

public struct Airport
{
    private string code;

    public Airport(string code)
    {
        // Check `code` for validity, throw exceptions if not valid

        this.code = code;
    }

    public override string ToString()
    {
        return code ?? (code = "---");
    }

    // int GetHashcode()

    // bool Equals(...)

    // bool operator ==(...)

    // bool operator !=(...)

    private bool Equals(Airport other)
    {
        if (other == null)
            // Even if this method is private, guard against null pointers
            return false;

        if (ToString() == "---" || other.ToString() == "---")
            // "Default" values should never match anything, even themselves
            return false;

        // Do a case insensitive comparison to enforce logic that airport
        // codes are not case sensitive
        return string.Equals(
            ToString(),
            other.ToString(),
            StringComparison.InvariantCultureIgnoreCase);
    }
}

สถานการณ์กรณีที่เลวร้ายยิ่งกว่าการแปลงdefault(Airport)เป็นสตริงจะพิมพ์ออกมา"---"และส่งกลับค่า false เมื่อเปรียบเทียบกับรหัสสนามบินอื่นที่ถูกต้อง รหัสสนามบิน "เริ่มต้น" ใด ๆ ที่ตรงกับสิ่งใดรวมถึงรหัสสนามบินเริ่มต้นอื่น ๆ

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

ฉันจะงอกฎเล็กน้อยที่นี่เพราะสิ่งนั้น


คำตอบเดิม (มีข้อผิดพลาดจริงบางประการ)

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

public Airport()
{
    throw new InvalidOperationException("...");
}

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

public Airport()
{
    Code = "---";
}

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

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


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

@ Matthew: การใช้คลาสทำให้คุณมีปัญหาเรื่องประสิทธิภาพหรือไม่? ถ้าไม่เช่นนั้นให้พลิกเหรียญเพื่อตัดสินใจว่าจะใช้อะไร
Greg Burghardt

4
@ Matthew: จริงๆสิ่งที่สำคัญคือคุณรวมศูนย์ตรรกะตรรกะของการทำให้รหัสและการเปรียบเทียบเป็นปกติ หลังจากนั้น "คลาสเทียบกับ struct" เป็นเพียงการอภิปรายทางวิชาการจนกว่าคุณจะวัดผลกระทบที่ใหญ่พอในการปฏิบัติงานเพื่อปรับเวลานักพัฒนาเพิ่มเติมเพื่อให้มีการอภิปรายทางวิชาการ
Greg Burghardt

1
นั่นเป็นเรื่องจริงฉันไม่รังเกียจที่จะมีการอภิปรายทางวิชาการเป็นครั้งคราวหากมันช่วยฉันในการสร้างวิธีการแก้ปัญหาที่ดีกว่าในอนาคต
Matthew

@ Matthew: ใช่คุณพูดถูก พวกเขาพูดว่า "พูดคุยราคาถูก" แน่นอนว่าราคาถูกกว่าไม่คุยและสร้างบางสิ่งที่ไม่ดี :)
Greg Burghardt

13

ใช้รูปแบบ Flyweight

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

ข้อดีเล็กน้อยอย่างหนึ่งเพิ่มเติม (อย่างน้อยใน Java, ไม่แน่ใจเกี่ยวกับ C #) คือคุณไม่จำเป็นต้องเขียนequals()วิธีการ, ==จะทำได้ง่าย hashcode()สำหรับเดียวกัน


3
การใช้งานที่ยอดเยี่ยมของรูปแบบฟลายเวท
Neil

2
สมมติว่า OP ยังคงใช้ struct และไม่ใช่คลาสไม่ใช่string interning ที่จัดการค่าสตริงที่นำกลับมาใช้ใหม่ได้หรือไม่ structs มีชีวิตอยู่บนสแต็กแล้วสตริงจะถูกนำมาใช้ซ้ำเพื่อหลีกเลี่ยงค่าที่ซ้ำกันในหน่วยความจำ จะได้ประโยชน์อะไรเพิ่มเติมจากรูปแบบฟลายเวท?
Flater

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

@ Flater จุดที่เหมาะสม ฉันจะบอกว่าไม่จำเป็นต้องให้โปรแกรมเมอร์ระดับรองลงมาให้เหตุผลเกี่ยวกับกองซ้อนกับกอง ดูเพิ่มเติมที่การเพิ่มของฉัน - ไม่จำเป็นต้องเขียนเท่ากับ ()
user949300

1
@Greg Burghardt หากgetAirportOrCreate()มีการซิงโครไนซ์รหัสอย่างถูกต้องไม่มีเหตุผลทางเทคนิคที่คุณไม่สามารถสร้างสนามบินใหม่ได้ตามต้องการระหว่างรันไทม์ อาจมีเหตุผลทางธุรกิจ
user949300

3

ฉันไม่ใช่โปรแกรมเมอร์ขั้นสูงโดยเฉพาะ แต่นี่จะไม่เป็นการใช้ที่สมบูรณ์แบบสำหรับ Enum ใช่ไหม

มีหลายวิธีในการสร้างคลาส enum จากรายการหรือสตริง นี่คือสิ่งที่ฉันเคยเห็นในอดีตไม่แน่ใจว่ามันเป็นวิธีที่ดีที่สุด

https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/


2
เมื่อมีค่าที่อาจแตกต่างกันหลายพันค่า (เช่นในกรณีที่มีรหัสสนามบิน) ค่า Enum ก็ไม่สามารถใช้ได้จริง
Ben Cottrell

ใช่ แต่ลิงค์ที่ฉันโพสต์เป็นวิธีโหลดสตริงเป็น enums นี่คือลิงค์อื่นเพื่อโหลดตารางการค้นหาเป็น enum มันอาจเป็นงานนิดหน่อย แต่จะใช้ประโยชน์จากพลังของ enums exceptionnotfound.net/…
อดัม B

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

@BenCottrell ที่แม่นยำสำหรับเทมเพลต gen gen และ T4
RubberDuck

3

หนึ่งในเหตุผลที่คุณเห็นกิจกรรม GC มากขึ้นคือคุณกำลังสร้างสตริงที่สองในขณะนี้ - .ToUpperInvariant()เวอร์ชันของสตริงเดิม สตริงเดิมมีสิทธิ์ได้รับ GC ทันทีหลังจากที่ตัวสร้างรันและสตริงที่สองมีสิทธิ์ในเวลาเดียวกันกับAirportวัตถุ คุณอาจย่อให้เล็กสุดในรูปแบบอื่น (หมายเหตุพารามิเตอร์ที่สามถึงstring.Equals()):

public sealed class Airport : IEquatable<Airport>
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0])
                             || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.",
                nameof(code));
        }

        Code = code;
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code; // TODO: Upper-case it here if you really need to for display.
    }

    public bool Equals(Airport other)
    {
        return string.Equals(Code, other?.Code, StringComparison.InvariantCultureIgnoreCase);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

นี่ไม่ได้ให้รหัสแฮชที่แตกต่างกันสำหรับสนามบินที่เท่ากัน (แต่เป็นตัวพิมพ์ใหญ่ต่างกัน)
Hero Wanders

ใช่ฉันจะจินตนาการอย่างนั้น Dangit
Jesse C. Slicer

นี่เป็นจุดที่ดีมากไม่เคยคิดเลยฉันจะดูการเปลี่ยนแปลงเหล่านี้
Matthew

1
ในเรื่องที่เกี่ยวกับGetHashCodeควรใช้StringComparer.OrdinalIgnoreCase.GetHashCode(Code)หรือคล้ายกัน
Matthew
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.