web-dev-qa-db-ja.com

C#のインターフェイスベースのプログラミングによる演算子のオーバーロード

バックグラウンド

現在のプロジェクトでインターフェイスベースのプログラミングを使用していて、演算子(特に、EqualおよびInequality演算子)をオーバーロードすると問題が発生しました。


仮定

  • C#3.0、.NET 3.5、およびVisual Studio 2008を使用しています

更新-次の仮定は誤りでした!

  • Operator ==ではなくEqualsを使用するためにすべての比較を要求することは、特にタイプをライブラリ(コレクションなど)に渡す場合、実行可能なソリューションではありません。

Operator ==の代わりにEqualsを使用する必要があることを心配した理由は、operator ==の代わりにEqualsを使用するか、またはそれを提案するという.NETガイドラインのどこにも見つからなかったためです。しかし、再度読んだ後 EqualsとOperator ==をオーバーライドするためのガイドライン== 私はこれを見つけました:

デフォルトでは、演算子==は、2つの参照が同じオブジェクトを示しているかどうかを判別することにより、参照の等価性をテストします。したがって、この機能を利用するために、参照型に演算子==を実装する必要はありません。型が不変である場合、つまりインスタンスに含まれるデータを変更できない場合、演算子==をオーバーロードして、参照の等価性ではなく値の等価性を比較すると便利です。これは、不変のオブジェクトとして、それらは長さ同じ価値があるからです。不変でない型の演算子==をオーバーライドすることはお勧めできません。

そしてこれ Equatable Interface

IEquatableインターフェイスは、Contains、IndexOf、LastIndexOf、Removeなどのメソッドで同等性をテストするときに、Dictionary、List、LinkedListなどの一般的なコレクションオブジェクトによって使用されます。ジェネリックコレクションに格納される可能性のあるオブジェクトに対して実装する必要があります。


拘束

  • どのようなソリューションでも、オブジェクトをインターフェイスから具象型にキャストする必要はありません。

問題

  • Operator ==の両側がインターフェースである場合は常に、基になる具象型からのoperator ==オーバーロードメソッドシグネチャが一致しないため、デフォルトのObject operator ==メソッドが呼び出されます。
  • クラスで演算子をオーバーロードする場合、バイナリ演算子のパラメーターの少なくとも1つが包含型である必要があります。そうでない場合、コンパイラエラーが生成されます(エラーBC33021 http://msdn.Microsoft.com/en-us /library/watt39ff.aspx
  • インターフェイスで実装を指定することはできません

問題を示す以下のコードと出力を参照してください。


質問

インターフェイスベースのプログラミングを使用する場合、クラスに適切な演算子オーバーロードをどのように提供しますか?


参考文献

==演算子(C#リファレンス)

定義済みの値タイプの場合、等価演算子(==)は、そのオペランドの値が等しい場合はtrueを返し、それ以外の場合はfalseを返します。文字列以外の参照型の場合、2つのオペランドが同じオブジェクトを参照している場合、==はtrueを返します。文字列型の場合、==は文字列の値を比較します。


こちらもご覧ください


コード

using System;

namespace OperatorOverloadsWithInterfaces
{
    public interface IAddress : IEquatable<IAddress>
    {
        string StreetName { get; set; }
        string City { get; set; }
        string State { get; set; }
    }

    public class Address : IAddress
    {
        private string _streetName;
        private string _city;
        private string _state;

        public Address(string city, string state, string streetName)
        {
            City = city;
            State = state;
            StreetName = streetName;
        }

        #region IAddress Members

        public virtual string StreetName
        {
            get { return _streetName; }
            set { _streetName = value; }
        }

        public virtual string City
        {
            get { return _city; }
            set { _city = value; }
        }

        public virtual string State
        {
            get { return _state; }
            set { _state = value; }
        }

        public static bool operator ==(Address lhs, Address rhs)
        {
            Console.WriteLine("Address operator== overload called.");
            // If both sides of the argument are the same instance or null, they are equal
            if (Object.ReferenceEquals(lhs, rhs))
            {
                return true;
            }

            return lhs.Equals(rhs);
        }

        public static bool operator !=(Address lhs, Address rhs)
        {
            return !(lhs == rhs);
        }

        public override bool Equals(object obj)
        {
            // Use 'as' rather than a cast to get a null rather an exception
            // if the object isn't convertible
            Address address = obj as Address;
            return this.Equals(address);
        }

        public override int GetHashCode()
        {
            string composite = StreetName + City + State;
            return composite.GetHashCode();
        }

        #endregion

        #region IEquatable<IAddress> Members

        public virtual bool Equals(IAddress other)
        {
            // Per MSDN documentation, x.Equals(null) should return false
            if ((object)other == null)
            {
                return false;
            }

            return ((this.City == other.City)
                && (this.State == other.State)
                && (this.StreetName == other.StreetName));
        }

        #endregion
    }

    public class Program
    {
        static void Main(string[] args)
        {
            IAddress address1 = new Address("seattle", "washington", "Awesome St");
            IAddress address2 = new Address("seattle", "washington", "Awesome St");

            functionThatComparesAddresses(address1, address2);

            Console.Read();
        }

        public static void functionThatComparesAddresses(IAddress address1, IAddress address2)
        {
            if (address1 == address2)
            {
                Console.WriteLine("Equal with the interfaces.");
            }

            if ((Address)address1 == address2)
            {
                Console.WriteLine("Equal with Left-hand side cast.");
            }

            if (address1 == (Address)address2)
            {
                Console.WriteLine("Equal with Right-hand side cast.");
            }

            if ((Address)address1 == (Address)address2)
            {
                Console.WriteLine("Equal with both sides cast.");
            }
        }
    }
}

出力

Address operator== overload called
Equal with both sides cast.
71
Zach Burlingame

短い答え:2つ目の仮定には誤りがあると思います。 Equals()は、2つのオブジェクトのセマンティックの等価性をチェックする正しい方法であり、_operator ==_ではありません。


長い答え:演算子のオーバーロード解決は実行時ではなくコンパイル時に実行されます

コンパイラーは、演算子を適用するオブジェクトのタイプを明確に知ることができない限り、コンパイルされません。コンパイラーは、IAddressが_==_のオーバーライドが定義されたものであることを確認できないため、_operator ==_のデフォルトの_System.Object_実装にフォールバックします。

これをより明確に表示するには、Addressに対して_operator +_を定義し、2つのIAddressインスタンスを追加してみてください。Addressに明示的にキャストしない限り、コンパイルに失敗します。どうして?コンパイラは特定のIAddressAddressであることを認識できず、_operator +_にフォールバックするデフォルトの_System.Object_実装がないためです。


フラストレーションの一部は、Objectが_operator ==_を実装し、すべてがObjectであるため、コンパイラがすべての_a == b_のような操作を正常に解決できるためです。タイプ。 _==_をオーバーライドした場合、同じ動作が見られるはずでしたが、そうではありませんでした。これは、コンパイラが見つけることができる最も一致するものが元のObject実装であるためです。

Operator ==ではなくEqualsを使用するためにすべての比較を要求することは、特にタイプをライブラリ(コレクションなど)に渡す場合、実行可能なソリューションではありません。

私の見解では、これはまさにあなたがしなければならないことです。 Equals()は、2つのオブジェクトの意味的等価性をチェックする正しい方法です。意味的等価性は、参照の等価性。この場合、何も変更する必要はありません。他のケースでは、例のように、参照の等価性よりも強い等価性コントラクトが必要な場合は、Equalsをオーバーライドします。たとえば、社会保障番号が同じ場合は2つのPersonsが等しいと見なし、同じVINの場合は2つのVehiclesを等しいと見なすことができます。

ただし、Equals()と_operator ==_は同じものではありません。 _operator ==_をオーバーライドする必要がある場合は常に、Equals()をオーバーライドする必要がありますが、その逆はほとんどありません。 _operator ==_は、より構文上の便宜です。一部のCLR言語(Visual Basic.NETなど)では、等価演算子をオーバーライドすることもできません。

56
John Feminella

私たちは同じ問題に遭遇し、優れた解決策を見つけました:カスタムパターンの再シャープ。

すべてのユーザーが自分のカタログに加えて共通のグローバルパターンカタログを使用するように構成し、SVNに配置して、すべてのユーザーがバージョン管理および更新できるようにしました。

カタログには、システムで間違っていることがわかっているすべてのパターンが含まれています。

_$i1$ == $i2$_(ここで、i1とi2は、インターフェイスタイプの、または派生です。

置換パターンは

$i1$.Equals($i2$)

重大度は「エラーとして表示」です。

同様に、_$i1$ != $i2$_

お役に立てれば。追伸グローバルカタログはResharper 6.1(EAP)の機能で、まもなく最終版としてマークされます。

更新:私は Resharper Issue を提出し、それがnullと比較しない限り、すべてのインターフェイス '=='に警告をマークしました。価値のある機能だと思ったら投票してください。

Update2:Resharperには、役立つ[CannotApplyEqualityOperator]属性もあります。

4
Yurik