воскресенье, 1 июля 2012 г.

Как написать Equatable или Comparable тип (#2)

В прошлый раз была предложена реализация сравнения объектов на равенство, для унификации которого служат методы типа Object и интерфейс IEquatable<>. Теперь рассмотрим самый простой способ реализовать сравнение объектов на "больше-меньше". Ниже снова будет не только множество скучного кода и ещё более скучных пояснений, но и сниппеты для упрощения его, кода, использования.

Но для начала ещё раз вспомним цель: получить максимально простую, шаблонную реализацию, с минимумом дублирования кода. Вся логика будет в одном единственном методе. Все остальные методы, прямо или косвенно, будут приводить к нему. А методы будут следующие: реализация (явная) IComparable, реализация IComparable<>, а так же определение операторов <, >, <= и >=. Как и в прошлый раз, рассмотрим типы:

using System;

internal struct MyValue
{
  public MyValue(int first, int? second) : this() {
    First = first;
    Second = second;
  }

  public int First { get; private set; }
  public int? Second { get; private set; }
}

internal sealed class MyData
{
  public MyData(string name, MyValue value) {
    Name = name ?? String.Empty;
    Value = value;
  }

  public string Name { get; private set; }
  public MyValue Value { get; private set; }
}

Реализация для значимых типов будет такой:

using System;
using System.Collections.Generic;

internal struct MyValue : IComparable, IComparable<MyValue>
{
  public MyValue(int first, int? second)
    : this() {
    First = first;
    Second = second;
  }

  public int First { get; private set; }
  public int? Second { get; private set; }

  #region IComparable Members

  int IComparable.CompareTo(object obj) {
    if(!(obj is MyValue)) {
      throw new ArgumentException("Argument is not the same type as this instance.", "obj");
    }//if

    return CompareTo((MyValue)obj);
  }

  #endregion IComparable Members

  #region IComparable<MyValue> Members

  public int CompareTo(MyValue other) {
    var compare = Comparer<int>.Default.Compare(First, other.First);
    if(compare == 0) {
      compare = Comparer<int?>.Default.Compare(Second, other.Second);
    }//if
    return compare;
  }

  #endregion IComparable<MyValue> Members

  public static bool operator <(MyValue left, MyValue right) {
    return left.CompareTo(right) < 0;
  }

  public static bool operator >(MyValue left, MyValue right) {
    return left.CompareTo(right) > 0;
  }

  public static bool operator <=(MyValue left, MyValue right) {
    return !(left > right);
  }

  public static bool operator >=(MyValue left, MyValue right) {
    return !(left < right);
  }
}

Особенности:

  • IComparable.CompareTo(object) первым делом проверяет тип аргумента, и если он не эквивалентен типу текущего экземпляра, выбрасывает исключение. Таков контракт этого метода в интерфейсе.
  • CompareTo(MyValue) попарно сравнивает поля текущего экземпляра с переданным образцом до тех пор, пока одно из сравнений не даст не-нулевой результат. Это и будет результатом сравнения текущего экземпляра и образца.
  • Операторы < и > сравнивают результат вызова CompareTo(MyValue) с нулём, а нестрогие операторы <= и >= реализованы посредством строгих.
  • Пространство имён System.Collections.Generic подключено из-за использования класса Comparer<>.

Реализация для ссылочных типов:

using System;
using System.Collections.Generic;

internal sealed class MyData : IComparable, IComparable<MyData>
{
  public MyData(string name, MyValue value) {
    Name = name ?? String.Empty;
    Value = value;
  }

  public string Name { get; private set; }
  public MyValue Value { get; private set; }

  #region IComparable Members

  int IComparable.CompareTo(object obj) {
    var other = obj as MyData;
    if(other == null) {
      if(obj == null) {
        return 1; // Some value, that greater then zero.
      } else {
        throw new ArgumentException("Argument is not the same type as this instance.", "obj");
      }//if
    }//if

    return CompareTo(other);
  }

  #endregion IComparable Members

  #region IComparable<MyData> Members

  public int CompareTo(MyData other) {
    if(other == null) {
      return 1; // Some value, that greater then zero.
    }//if

    var compare = StringComparer.Ordinal.Compare(Name, other.Name);
    if(compare == 0) {
      compare = Comparer<MyValue>.Default.Compare(Value, other.Value);
    }//if
    return compare;
  }

  #endregion IComparable<MyData> Members

  public static bool operator <(MyData left, MyData right) {
    return right != null && right.CompareTo(left) > 0;
  }

  public static bool operator >(MyData left, MyData right) {
    return left != null && left.CompareTo(right) > 0;
  }

  public static bool operator <=(MyData left, MyData right) {
    return !(left > right);
  }

  public static bool operator >=(MyData left, MyData right) {
    return !(left < right);
  }
}

Особенности:

  • Реализация IComparable.CompareTo(object), как и операторы < и >, учитывают, что аргумент (или оба) могут содержать пустую ссылку (null). Не-null-объект всегда больше null и два null-объекта считаются равными.

Ну вот и всё, в завершении осталось опубликовать снипетты, которые позволяют быстро добавить описанную реализацию к вашему типу. Сниппет для значимого типа:

<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Implements IComparable and IComparable<> interfaces for a value type</Title>
      <Shortcut>comparablestruct</Shortcut>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="false">
          <ID>TypeName</ID>
          <Function>ClassName()</Function>
        </Literal>
        <Literal Editable="false">
          <ID>ArgumentException</ID>
          <Function>SimpleTypeName(global::System.ArgumentException)</Function>
        </Literal>
        <Literal Editable="false">
          <ID>NotImplementedException</ID>
          <Function>SimpleTypeName(global::System.NotImplementedException)</Function>
        </Literal>
      </Declarations>
      <Code Language="CSharp">
<![CDATA[#region IComparable Members

int IComparable.CompareTo(object obj) {
  if(!(obj is $TypeName$)) {
    throw new $ArgumentException$("Argument is not the same type as this instance.", "obj");
  }//if

  return CompareTo(($TypeName$)obj);
}

#endregion IComparable Members

#region IComparable<$TypeName$> Members

public int CompareTo($TypeName$ other) {
  $end$throw new $NotImplementedException$();
}

#endregion IComparable<$TypeName$> Members

public static bool operator <($TypeName$ left, $TypeName$ right) {
  return left.CompareTo(right) < 0;
}

public static bool operator >($TypeName$ left, $TypeName$ right) {
  return left.CompareTo(right) > 0;
}

public static bool operator <=($TypeName$ left, $TypeName$ right) {
  return !(left > right);
}

public static bool operator >=($TypeName$ left, $TypeName$ right) {
  return !(left < right);
}
]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Сниппет для ссылочного типа:

<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Implements IComparable and IComparable<> interface for a reference type</Title>
      <Shortcut>comparableclass</Shortcut>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="false">
          <ID>TypeName</ID>
          <Function>ClassName()</Function>
        </Literal>
        <Literal Editable="false">
          <ID>ArgumentException</ID>
          <Function>SimpleTypeName(global::System.ArgumentException)</Function>
        </Literal>
        <Literal Editable="false">
          <ID>NotImplementedException</ID>
          <Function>SimpleTypeName(global::System.NotImplementedException)</Function>
        </Literal>
      </Declarations>
      <Code Language="CSharp">
<![CDATA[#region IComparable Members

int IComparable.CompareTo(object obj) {
  var other = obj as $TypeName$;
  if(other == null) {
    if(obj == null) {
      return 1; // Some value, that greater then zero.
    } else {
      throw new $ArgumentException$("Argument is not the same type as this instance.", "obj");
    }//if
  }//if
  return CompareTo(other);
}

#endregion IComparable Members

#region IComparable<$TypeName$> Members

public int CompareTo($TypeName$ other) {
  if(other == null) {
    return 1; // Some value, that greater then zero.
  }//if

  $end$throw new $NotImplementedException$();
}

#endregion IComparable<$TypeName$> Members

public static bool operator <($TypeName$ left, $TypeName$ right) {
  return right != null && right.CompareTo(left) > 0;
}

public static bool operator >($TypeName$ left, $TypeName$ right) {
  return left != null && left.CompareTo(right) > 0;
}

public static bool operator <=($TypeName$ left, $TypeName$ right) {
  return !(left > right);
}

public static bool operator >=($TypeName$ left, $TypeName$ right) {
  return !(left < right);
}
]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>