cấu trúc với giá trị mặc định vô nghĩa


12

Trong hệ thống của tôi, tôi thường xuyên hoạt động với mã sân bay ( "YYZ", "LAX", "SFO", vv), chúng luôn luôn theo cùng định dạng chính xác (3 lá thư, thể hiện dưới dạng chữ hoa). Hệ thống thường xử lý 25-50 mã (khác nhau) cho mỗi yêu cầu API, với tổng số hơn một nghìn phân bổ, chúng được chuyển qua nhiều lớp trong ứng dụng của chúng tôi và được so sánh khá bình thường.

Chúng tôi đã bắt đầu chỉ bằng cách chuyển các chuỗi xung quanh, hoạt động tốt một chút nhưng chúng tôi nhanh chóng nhận thấy rất nhiều lỗi lập trình bằng cách gửi mã sai ở đâu đó mã 3 chữ số được mong đợi. Chúng tôi cũng gặp phải các vấn đề trong đó chúng tôi phải thực hiện một so sánh không phân biệt chữ hoa chữ thường và thay vào đó là không, dẫn đến lỗi.

Từ đó, tôi quyết định dừng các chuỗi xung quanh và tạo một Airportlớp, trong đó có một hàm tạo duy nhất lấy và xác nhận mã sân bay.

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);
    }
}

Điều này làm cho mã của chúng tôi dễ hiểu hơn nhiều và chúng tôi đã đơn giản hóa việc kiểm tra đẳng thức, từ điển / tập hợp sử dụng. Bây giờ chúng ta biết rằng nếu các phương thức của chúng ta chấp nhận một Airportthể hiện rằng nó sẽ hoạt động theo cách chúng ta mong đợi, thì nó đã đơn giản hóa việc kiểm tra phương thức của chúng ta thành kiểm tra tham chiếu null.

Tuy nhiên, điều tôi nhận thấy là bộ sưu tập rác đã chạy thường xuyên hơn, mà tôi đã theo dõi rất nhiều trường hợp Airportthu thập.

Giải pháp của tôi cho việc này là chuyển đổi classthành a struct. Chủ yếu chỉ là thay đổi từ khóa, ngoại trừ GetHashCodeToString:

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

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

Để xử lý trường hợp default(Airport)được sử dụng.

Những câu hỏi của tôi:

  1. Việc tạo ra một Airportlớp hoặc cấu trúc một giải pháp tốt nói chung, hay tôi đang giải quyết vấn đề sai / giải quyết nó sai cách bằng cách tạo ra loại? Nếu nó không phải là một giải pháp tốt, thì giải pháp nào tốt hơn?

  2. Ứng dụng của tôi nên xử lý các trường hợp default(Airport)sử dụng như thế nào? Một loại default(Airport)là vô nghĩa đối với ứng dụng của tôi, vì vậy tôi đã làm if (airport == default(Airport) { throw ... }ở những nơi mà việc lấy một thể hiện của Airport(và thuộc tính của nó Code) là rất quan trọng đối với hoạt động.

Lưu ý: Tôi đã xem lại các câu hỏi C # / VB struct - làm thế nào để tránh trường hợp có giá trị mặc định bằng 0, được coi là không hợp lệ đối với cấu trúc đã cho? Sử dụng struct hoặc không trước khi đặt câu hỏi của tôi, tuy nhiên tôi nghĩ rằng các câu hỏi của tôi đủ khác nhau để đảm bảo bài đăng của chính nó.


7
Liệu thu gom rác thải có một tài liệu ảnh hưởng đến cách Thực hiện ứng dụng của bạn? Nói cách khác, nó có vấn đề?
Robert Harvey

Dù sao, vâng, giải pháp lớp học là một "tốt". Cách bạn biết là nó đã giải quyết vấn đề của bạn mà không tạo ra bất kỳ vấn đề mới nào.
Robert Harvey

2
Một cách bạn có thể giải quyết default(Airport)vấn đề là đơn giản là không cho phép các trường hợp mặc định. Bạn có thể làm điều đó bằng cách viết một hàm tạo không tham số và ném InvalidOperationExceptionhoặc NotImplementedExceptiontrong đó.
Robert Harvey

3
Mặt khác, thay vì xác nhận rằng chuỗi khởi tạo của bạn thực tế là 3 ký tự alpha, tại sao không so sánh nó với danh sách hữu hạn của tất cả các mã sân bay (ví dụ: github.com/datasets/airport-codes hoặc tương tự)?
Dan Pichelman

2
Tôi sẵn sàng đặt cược một số loại bia rằng đây không phải là gốc rễ của vấn đề hiệu suất. Một máy tính xách tay bình thường có thể phân bổ theo thứ tự 10M đối tượng / giây.
Esben Skov Pedersen

Câu trả lời:


6

Cập nhật: Tôi viết lại câu trả lời của mình để giải quyết một số giả định không chính xác về cấu trúc C #, cũng như OP thông báo cho chúng tôi trong các nhận xét rằng các chuỗi được sử dụng đang được sử dụng.


Nếu bạn có thể kiểm soát dữ liệu đi vào hệ thống của mình, hãy sử dụng một lớp như bạn đã đăng trong câu hỏi của mình. Nếu ai đó chạy default(Airport)họ sẽ nhận lại một nullgiá trị. Hãy chắc chắn viết Equalsphương thức riêng tư của bạn để trả về false bất cứ khi nào so sánh các đối tượng sân bay null và sau đó để NullReferenceExceptionmã số bay đi nơi khác trong mã.

Tuy nhiên, nếu bạn đang lấy dữ liệu vào hệ thống từ các nguồn bạn không kiểm soát, bạn không cần thiết phải làm hỏng toàn bộ chuỗi. Trong trường hợp này, một cấu trúc là lý tưởng cho thực tế đơn giản default(Airport)sẽ cung cấp cho bạn một cái gì đó ngoài một nullcon trỏ. Tạo một giá trị rõ ràng để biểu thị "không có giá trị" hoặc "giá trị mặc định" để bạn có thứ gì đó để in trên màn hình hoặc trong tệp nhật ký (ví dụ như "---"). Trên thực tế, tôi sẽ chỉ giữ coderiêng tư và không để lộ một Codetài sản nào cả - chỉ tập trung vào hành vi ở đây.

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);
    }
}

Trường hợp xấu hơn chuyển default(Airport)thành chuỗi in ra "---"và trả về sai khi so sánh với các mã Sân bay hợp lệ khác. Bất kỳ mã sân bay "mặc định" nào cũng không khớp, kể cả các mã sân bay mặc định khác.

Đúng, các cấu trúc có nghĩa là các giá trị được phân bổ trên ngăn xếp và bất kỳ con trỏ nào để bộ nhớ heap về cơ bản đều phủ nhận các lợi thế về hiệu suất của các cấu trúc, nhưng trong trường hợp này, giá trị mặc định của một cấu trúc có ý nghĩa và cung cấp thêm một số kháng đạn cho phần còn lại của ứng dụng.

Tôi sẽ bẻ cong các quy tắc một chút ở đây, vì điều đó.


Câu trả lời gốc (với một số lỗi thực tế)

Nếu bạn có thể kiểm soát dữ liệu đi vào hệ thống của mình, tôi sẽ làm như Robert Harvey đã đề xuất trong các nhận xét: Tạo một hàm tạo không tham số và đưa ra một ngoại lệ khi nó được gọi. Điều này ngăn dữ liệu không hợp lệ xâm nhập vào hệ thống thông qua default(Airport).

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

Tuy nhiên, nếu bạn đang lấy dữ liệu vào hệ thống từ các nguồn bạn không kiểm soát, bạn không cần thiết phải làm hỏng toàn bộ chuỗi. Trong trường hợp này, bạn có thể tạo mã sân bay không hợp lệ, nhưng làm cho nó có vẻ như là một lỗi rõ ràng. Điều này sẽ liên quan đến việc tạo một hàm tạo không tham số và đặt Codethành một cái gì đó như "---":

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

Vì bạn đang sử dụng stringlàm Mã, nên không có điểm nào trong việc sử dụng cấu trúc. Cấu trúc được phân bổ trên ngăn xếp, chỉ có Codephân bổ dưới dạng con trỏ tới một chuỗi trong bộ nhớ heap, do đó không có sự khác biệt ở đây giữa lớp và struct.

Nếu bạn thay đổi mã sân bay thành một mảng gồm 3 mục của char thì một cấu trúc sẽ được phân bổ đầy đủ trên ngăn xếp. Ngay cả khi đó, khối lượng dữ liệu không lớn để tạo ra sự khác biệt.


Nếu ứng dụng của tôi đang sử dụng các chuỗi nội bộ cho thuộc Codetính, điều đó có thay đổi sự biện minh của bạn về điểm của chuỗi trong bộ nhớ heap không?
Matthew

@Matthew: Việc sử dụng một lớp học có gây ra vấn đề về hiệu suất không? Nếu không, sau đó lật một đồng xu để quyết định sử dụng.
Greg Burghardt

4
@Matthew: Thực sự điều quan trọng là bạn tập trung vào logic rắc rối của việc bình thường hóa các mã và so sánh. Sau đó, "lớp so với cấu trúc" chỉ là một cuộc thảo luận học thuật, cho đến khi bạn đo lường được tác động đủ lớn trong hiệu suất để biện minh cho thời gian phát triển thêm để có một cuộc thảo luận học thuật.
Greg Burghardt

1
Điều đó đúng, tôi không ngại thỉnh thoảng có một cuộc thảo luận học thuật nếu điều đó giúp tôi tạo ra các giải pháp thông tin tốt hơn trong tương lai.
Matthew

@Matthew: Yup, bạn hoàn toàn đúng. Họ nói "nói là rẻ." Nó chắc chắn rẻ hơn so với việc không nói và xây dựng một cái gì đó kém. :)
Greg Burghardt

13

Sử dụng mô hình Fly trọng

Vì Sân bay, chính xác, không thay đổi, nên không cần tạo nhiều hơn một thể hiện của bất kỳ trường hợp cụ thể nào, giả sử, SFO. Sử dụng Hashtable hoặc tương tự (lưu ý, tôi là người Java, không phải C # nên chi tiết chính xác có thể thay đổi), để lưu trữ Sân bay khi chúng được tạo. Trước khi tạo một cái mới, hãy kiểm tra Hashtable. Bạn không bao giờ giải phóng Sân bay, do đó, GC không cần phải giải phóng chúng.

Một lợi thế nhỏ khác (ít nhất là trong Java, không chắc chắn về C #) là bạn không cần phải viết equals() phương thức, một việc đơn giản ==sẽ làm. Tương tự cho hashcode().


3
Sử dụng tuyệt vời của các mẫu bay.
Neil

2
Giả sử OP tiếp tục sử dụng một cấu trúc chứ không phải một lớp, không phải chuỗi thực tập đã xử lý các giá trị chuỗi có thể sử dụng lại? Các cấu trúc đã sống trên ngăn xếp, các chuỗi đã được sử dụng lại để tránh các giá trị trùng lặp trong bộ nhớ. Những lợi ích bổ sung nào sẽ đạt được từ mô hình cân nặng?
Flater

Một cái gì đó để coi chừng. Nếu một sân bay được thêm hoặc xóa, bạn sẽ muốn xây dựng một cách để làm mới danh sách tĩnh này mà không cần khởi động lại ứng dụng hoặc triển khai lại. Các sân bay không được thêm hoặc xóa thường xuyên, nhưng chủ doanh nghiệp có xu hướng hơi khó chịu khi một thay đổi đơn giản trở nên phức tạp này. "Tôi không thể thêm nó vào một nơi nào đó sao?! Tại sao chúng tôi phải lên lịch phát hành lại / khởi động lại ứng dụng và gây bất tiện cho khách hàng?" Nhưng tôi cũng đã nghĩ đến việc sử dụng một số loại bộ đệm tĩnh lúc đầu.
Greg Burghardt

@Flater Điểm hợp lý. Tôi muốn nói rằng ít cần các lập trình viên cơ sở lý luận về stack so với heap. Cộng với xem bổ sung của tôi - không cần phải viết bằng ().
user949300

1
@Greg Burghardt Nếu getAirportOrCreate()mã được đồng bộ hóa chính xác, không có lý do kỹ thuật nào bạn không thể tạo Sân bay mới khi cần trong thời gian chạy. Có thể có lý do kinh doanh.
user949300

3

Tôi không phải là một lập trình viên đặc biệt tiên tiến, nhưng liệu đây có phải là một cách sử dụng hoàn hảo cho Enum không?

Có nhiều cách khác nhau để xây dựng các lớp enum từ danh sách hoặc chuỗi. Đây là một trong những điều tôi đã thấy trong quá khứ, không chắc đó có phải là cách tốt nhất hay không.

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


2
Khi có tiềm năng hàng ngàn giá trị khác nhau (như trường hợp mã sân bay), một enum chỉ là không thực tế.
Ben Cottrell

Có, nhưng liên kết tôi đã đăng là làm thế nào để tải chuỗi dưới dạng enums. Đây là một liên kết khác để tải bảng tra cứu dưới dạng enum. Nó có thể là một chút công việc, nhưng sẽ tận dụng sức mạnh của enum. ngoại lệ, chú ý
Adam

1
Hoặc một danh sách các mã hợp lệ có thể được tải từ cơ sở dữ liệu hoặc tệp. Sau đó, một mã sân bay chỉ được kiểm tra trong danh sách đó. Đây là những gì bạn thường làm khi bạn không còn muốn mã hóa cứng các giá trị và / hoặc danh sách trở nên dài để quản lý.
Neil

@BenCottrell đó chính xác là mẫu mã gen và mẫu T4 dành cho.
RubberDuck

3

Một trong những lý do bạn thấy nhiều hoạt động của GC là vì bạn đang tạo một chuỗi thứ hai ngay bây giờ - .ToUpperInvariant()phiên bản của chuỗi gốc. Chuỗi ban đầu đủ điều kiện cho GC ngay sau khi hàm tạo chạy và chuỗi thứ hai đủ điều kiện cùng lúc với Airportđối tượng. Bạn có thể thu nhỏ nó theo cách khác (lưu ý tham số thứ ba đến 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);
    }
}

Điều này không mang lại mã băm khác nhau cho các Sân bay bằng nhau (nhưng được viết hoa khác nhau)?
Anh hùng lang thang

Vâng, tôi sẽ tưởng tượng như vậy. Nguy hiểm
Jesse C. Choper

Đây là một điểm rất tốt, không bao giờ nghĩ về nó, tôi sẽ xem xét thực hiện những thay đổi này.
Matthew

1
Liên quan đến GetHashCode, chỉ nên sử dụng StringComparer.OrdinalIgnoreCase.GetHashCode(Code)hoặc tương tự
Matthew
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.