無意味なデフォルト値を持つ構造体


12

私のシステムでは、私は頻繁に空港コード(で動作し"YYZ""LAX""SFO"、など)、彼らはまったく同じ形式(大文字として表現3文字)に常にあります。システムは通常、APIリクエストごとにこれらの(異なる)コードの25〜50を処理し、合計で1,000を超える割り当てが行われ、アプリケーションの多くのレイヤーを通過して、頻繁に同等性が比較されます。

最初は文字列を渡すだけで、少しはうまくいきましたが、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収集。

これに対する私の解決策は、に変換することでしclassstruct。ほとんどの場合、キーワードの変更でしたがGetHashCode、andを除きます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使用するかどうかを選択しますが、私の質問はそれ自体の投稿を保証するほど十分に異なると思います。


7
ガベージコレクションは、アプリケーションの実行方法に重大な影響を与えますか?言い換えれば、それは重要ですか?
ロバートハーヴェイ

とにかく、はい、クラスソリューションは「良い」ものでした。あなたが知っている方法は、新しいものを作成せずに問題を解決したことです。
ロバートハーベイ

2
default(Airport)問題を解決する1つの方法は、デフォルトのインスタンスを単に禁止することです。それには、パラメータなしのコンストラクタを記述して、その中にスローするInvalidOperationExceptionNotImplementedException、または挿入します。
ロバートハーヴェイ

3
補足として、初期化文字列が実際には3文字のアルファ文字であることを確認する代わりに、すべての空港コードの有限リスト(たとえば、github.com / datasets / airport-codesなど)と比較してみませんか?
ダンピチェルマン

2
これがパフォーマンスの問題の根本原因ではないことを、いくつかのビールに賭けるつもりです。通常のラップトップは、10Mオブジェクト/秒の順序で割り当てることができます。
エスベンスコフペダーセン

回答:


6

更新: C#構造体に関するいくつかの誤った仮定と、インターンされた文字列が使用されていることをコメントで知らせるOPに対処するために、答えを書き直しました。


システムに送られるデータを制御できる場合は、質問に投稿したクラスを使用してください。誰かが走っdefault(Airport)たら、彼らはnull値が返されます。EqualsnullのAirportオブジェクトを比較するときは常にfalseを返すプライベートメソッドを作成NullReferenceExceptionし、コードの別の場所にを飛ばしてください。

ただし、管理していないソースからシステムにデータを取り込む場合、スレッド全体をクラッシュさせる必要はありません。この場合、構造体は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 = "---";
}

stringコードとしてa を使用しているため、構造体を使用しても意味がありません。構造体はスタックにCode割り当てられ、ヒープメモリ内の文字列へのポインタとしてのみ割り当てられるため、ここではクラスと構造体の間に違いはありません。

空港コードをcharの3項目配列に変更すると、スタックに構造体が完全に割り当てられます。それでも、データ量は違いを生むほど大きくはありません。


私のアプリケーションがCodeプロパティにインターンされた文字列を使用している場合、ヒープメモリ内にある文字列のポイントに関する正当性が変わりますか?
マシュー

@Matthew:クラスを使用するとパフォーマンスの問題が発生しますか?そうでない場合は、コインを裏返して使用するものを決定します。
グレッグブルクハート

4
@Matthew:本当に重要なことは、コードと比較を正規化する面倒なロジックを集中化することです。その後、「クラス対構造」は、アカデミックディスカッションを行うための余分な開発者の時間を正当化するのに十分なほど大きなパフォーマンスへの影響を測定するまで、アカデミックディスカッションです。
グレッグブルクハート

1
それは本当です、将来的により良い情報に基づいたソリューションを作成するのに役立つなら、私は時々学術的な議論をしても構いません。
マシュー

@マシュー:うん、あなたは絶対に正しいです。彼らは「話は安い」と言う。話をしないで何かを粗末に構築するよりも確かに安いです。:)
グレッグブル

13

フライウェイトパターンを使用する

Airportは正しく、不変なので、特定のインスタンス(SFOなど)のインスタンスを複数作成する必要はありません。ハッシュテーブルなどを使用して(注:私はJavaの男であり、C#ではありませんので、正確な詳細は異なる場合があります)、空港の作成時にキャッシュします。新しいものを作成する前に、ハッシュテーブルをチェックインします。空港を解放することはないので、GCは空港を解放する必要がありません。

もう1つの小さな利点(少なくともJavaではC#についてはわかりません)はequals()、簡単な方法でメソッドを記述する必要がない==ことです。同じですhashcode()


3
フライウェイトパターンの優れた使用法。
ニール

2
OPがクラスではなく構造体を使用し続けると仮定すると、文字列インターンはすでに再利用可能な文字列値を処理していませんか?構造体は既にスタック上に存在し、文字列はメモリ内の値の重複を避けるために既に再利用されています。フライウェイトパターンからどのような追加の利益が得られますか?
18

気をつけるべきこと。空港が追加または削除された場合は、アプリケーションを再起動したり再デプロイしたりせずに、この静的リストを更新する方法で構築する必要があります。空港は頻繁に追加または削除されることはありませんが、単純な変更がこのように複雑になると、ビジネスオーナーは少し混乱する傾向があります。「どこかに追加するだけではいけませんか?!リリース/アプリケーションの再起動をスケジュールし、お客様に不便をかける必要があるのはなぜですか?」しかし、最初は何らかの静的キャッシュを使用することも考えていました。
グレッグブルクハート

@Flater合理的なポイント。私は、ジュニアプログラマーがスタックとヒープについて推論する必要が少なくなると思います。さらに、私の追加を参照してください-equals()を記述する必要はありません。
user949300

1
@Greg Burghardt getAirportOrCreate()コードが適切に同期されている場合、実行時に必要に応じて新しい空港を作成できないという技術的な理由はありません。ビジネス上の理由があるかもしれません。
user949300

3

私は特に高度なプログラマーではありませんが、これはEnumの完璧な使い方ではないでしょうか?

リストまたは文字列から列挙型クラスを構築するには、さまざまな方法があります。これは私が過去に見たものですが、それが最善の方法であるかどうかはわかりません。

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


2
空港コードの場合のように、潜在的に数千の異なる値がある場合、列挙型は実際的ではありません。
ベンコットレル

はい。ただし、私が投稿したリンクは、文字列を列挙型としてロードする方法です。ルックアップテーブルを列挙型として読み込む別のリンクを次に示します。少し手間がかかるかもしれませんが、列挙型の力を利用します。exceptionnotfound.net/...
アダム・B

1
または、データベースまたはファイルから有効なコードのリストをロードできます。次に、空港コードがチェックされ、そのリストに含まれます。これは、値をハードコーディングしたり、リストの管理が長くなりすぎたりする場合に通常行うことです。
ニール

@BenCottrellは、まさにコード生成とT4テンプレートの目的です。
ラバーダック

3

GCアクティビティが増えている理由の1つは、2番目の文字列(.ToUpperInvariant()元の文字列のバージョン)を作成しているためです。元の文字列はコンストラクターの実行直後にGCに適格であり、2番目の文字列はAirportオブジェクトと同時に適格です。別の方法で最小化できる場合があります(3番目のパラメーターに注意してください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);
    }
}

これは、等しい(ただし大文字が異なる)空港に対して異なるハッシュコードを生成しませんか?
ヒーローワンダーズ

ええ、そう思います。ダンギット。
ジェシーC.スライサー

これは非常に良い点であり、考えもしなかったので、これらの変更を検討します。
マシュー

1
に関してはGetHashCode、単に使用するStringComparer.OrdinalIgnoreCase.GetHashCode(Code)か、類似する必要があります
Matthew
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.