[C# 4 и 3] Построитель компараторов [Bug fixed]
От: _temp  
Дата: 22.03.15 03:40
Оценка: 36 (1)
По мотивам [C# 4 и 3] Построитель компараторов (Я так понимаю, на страрую тему запретили отвечать? Создам на всякий случай новую)

В компараторе внезапно обнаружилась древняя бага, проявляющаяся в создаваемом Comparer<>-е при сравнении нуллейбл-полей (сравнение проходит не верно). С EqualityComparer<> всё отлично (ошибок за всё время активного использования с момента создания выявлено не было).
  Фикс следующий
// 1. Вместо старых функций IsNull и IsNotNull делаем новые:

private static BinaryExpression IsNull(Expression expression) {
  if(expression == null) {
    throw new ArgumentNullException("expression");
  }//if

  if(IsTypeByReference(expression)) {
    return Expression.ReferenceEqual(expression, Null);
  } else {
    return Expression.Equal(expression, Null);
  }//if
}

private static BinaryExpression IsNotNull(Expression expression) {
  if(expression == null) {
    throw new ArgumentNullException("expression");
  }//if

  if(IsTypeByReference(expression)) {
    return Expression.ReferenceNotEqual(expression, Null);
  } else {
    return Expression.NotEqual(expression, Null);
  }//if
}

private static bool IsTypeByReference(Expression expression) {
  if(expression == null) {
    throw new ArgumentNullException("expression");
  }//if

  return IsTypeByReference(expression.Type);
}

private static bool IsTypeNullable(Expression expression) {
  if(expression == null) {
    throw new ArgumentNullException("expression");
  }//if

  var type = expression.Type;
  return IsTypeByReference(type) || type.IsValueType && Nullable.GetUnderlyingType(type) != null;
}

private static bool IsTypeByReference(Type type) {
  if(type == null) {
    throw new ArgumentNullException("type");
  }//if

  return type.IsClass || type.IsInterface;
}

// 2. Метод MakeCompare<P> должен быть таким (изменена ветка else)

protected virtual Expression MakeCompare<P>(Expression left, Expression right, IComparer<P> comparer) {
  if(left == null) {
    throw new ArgumentNullException("left");
  } else if(right == null) {
    throw new ArgumentNullException("right");
  }//if

  if(comparer != null) {
    // return comparer.Compare(left, right);
    Func<P, P, int> @delegate = comparer.Compare;
    var instance = Expression.Constant(comparer);
    return Expression.Call(instance, @delegate.Method, left, right);
  } else {
    // (left < right) ? -1 : (right < left ? 1 : 0);
    var compare = Expression.Condition(Expression.LessThan(left, right), MinusOne, Expression.Condition(Expression.LessThan(right, left), One, Zero));
    var code = (IsTypeNullable(left) ? 2 : 0) + (IsTypeNullable(right) ? 1 : 0);
    switch(code) {
    case 0: // "left" and "right" both are not nullable
      return compare;
    case 1: // "left" is not nullable and "right" is nullable
      // (object)right == null ? 1 : {compare};
      return Expression.Condition(IsNull(right), One, compare);
    case 2: // "left" is nullable and "right" is not nullable
      // (object)left == null ? -1 : {compare};
      return Expression.Condition(IsNull(left), MinusOne, compare);
    case 3: // "left" and "right" both are nullable
      // (object)left == null ? (right == null ? 0 : -1) : ((object)right == null ? 1 : {compare});
      return Expression.Condition(IsNull(left), Expression.Condition(IsNull(right), Zero, MinusOne), Expression.Condition(IsNull(right), One, compare));
    default:
      const string Format = "Invalid code: left.Type = {0}, right.Type = {1}, code = {2}.";
      var message = String.Format(Format, left.Type, right.Type, code);
      Debug.Fail(message);
      throw new InvalidOperationException(message);
    }//switch
  }//if
}

Тест для проверки:
class BaseData
{
  public BaseData(DateTime? test = null) {
    Test = test;
  }

  public DateTime? Test { get; private set; }
}

public static void CompareNullableProperty() {
  var comparer = new ComparerBuilder<BaseData>().Add(value => value.Test).ToComparer();
  var items = new[] {
    new BaseData(null),
    new BaseData(new DateTime(2015, 01, 01)),
    new BaseData(null),
    new BaseData(new DateTime(2015, 02, 01)),
    new BaseData(null),
    new BaseData(new DateTime(2015, 03, 01)),
  };
  Array.Sort(items, comparer);
  var previous = default(BaseData);
  foreach(var item in items) {
    if(previous != null) {
      Debug.Assert(previous.Test == null || previous.Test <= item.Test,String.Format("previous.Test2 == null || previous.Test2 [{0}] <= item.Test2 [{1}]", previous.Test, item.Test));
    }//if
    previous = item;
  }//for
}

Проект, кстати, теперь есть на гитхабе, но уже на шестом шарпе, но уже и не такой куцый.

Edit: Исправил код, убрав nameof и переименов вспомагательный метод.
Отредактировано 23.03.2015 10:59 _temp . Предыдущая версия . Еще …
Отредактировано 22.03.2015 9:30 _temp . Предыдущая версия .
Отредактировано 22.03.2015 3:42 _temp . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.