Почему сумма Linq-to-Objects из последовательности нулевых значений сама по себе является нулевой?

Как обычно, int? означает System.Nullable<int> (или System.Nullable`1[System.Int32]).

Предположим, что у вас есть встроенная память IEnumerable<int?> (например, List<int?>), назовем ее seq; то вы можете найти его сумму с помощью:

var seqSum = seq.Sum();

Конечно, это относится к перегрузке метода расширения int? IEnumerable<int?>.Sum() (документация), которая действительно является статическим методом на System.Linq.Enumerable.

Однако метод никогда не возвращает null, , поэтому почему тип возврата объявлен как Nullable<>? Даже в тех случаях, когда seq представляет собой пустую коллекцию или, в общем, коллекцию, все чьими элементами являются null значение типа int?, метод Sum все еще возвращает ноль, а не null.

Это видно из документации, но также из исходного кода System.Core.dll:

public static int? Sum(this IEnumerable<int?> source) { 
    if (source == null) throw Error.ArgumentNull("source"); 
    int sum = 0; 
    checked { 
        foreach (int? v in source) { 
            if (v != null) sum += v.GetValueOrDefault(); 
        } 
    } 
    return sum; 
} 

Обратите внимание, что существует только один оператор return, и его выражение Sum имеет тип int (который затем будет неявно преобразован в int? путем обертывания).

Кажется расточительным всегда обертывать возвращаемое значение. (При желании вызывающий абонент всегда мог сделать обертку неявно на его стороне.)

Кроме того, этот тип возврата может привести вызывающего пользователя к написанию кода, такого как if (!seqSum.HasValue) { /* logic to handle this */ }, который в действительности будет недоступен (факт, о котором компилятор С# не может знать).

Итак, почему этот возвращаемый параметр не просто объявлен как int без значения NULL?

Интересно, есть ли какая-либо выгода от того же типа возврата, что и int? IQueryable<int?>.Sum() (в классе System.Linq.Queryable). Этот последний метод может возвращать null на практике, если есть поставщики LINQ (возможно, LINQ to SQL?), Которые его реализуют.

Ответ 1

Несколько замечаний отметили, что на самом деле это не ответственно (или только мнение, основанное без официального ответа). Я не буду спорить об этом. Тем не менее, все еще можно провести анализ доступного кода и сформировать достаточно прочную теорию. Моя просто так, что это существующий шаблон MS.

Если вы просмотрите оставшуюся часть System.Linq.Enumerable, в частности связанные с математикой функции, вы начнете видеть шаблон, имеющий тенденцию возвращать тот же тип, что и входной параметр, если только возврат не имеет определенной причины другого типа.

Смотрите следующие функции:

Max():

public static int Max(this IEnumerable<int> source);
public static int? Max(this IEnumerable<int?> source);
public static long Max(this IEnumerable<long> source);
public static long? Max(this IEnumerable<long?> source);

Min():

public static int Min(this IEnumerable<int> source);
public static int? Min(this IEnumerable<int?> source);
public static long Min(this IEnumerable<long> source);
public static long? Min(this IEnumerable<long?> source);

Sum():

public static int Sum(this IEnumerable<int> source);
public static int? Sum(this IEnumerable<int?> source);
public static long Sum(this IEnumerable<long> source);
public static long? Sum(this IEnumerable<long?> source);

Для исключения из правила взгляните на Average...

public static double Average(this IEnumerable<int> source);
public static double? Average(this IEnumerable<int?> source);

Вы можете видеть, что он по-прежнему сохраняет тип Nullable<T>, однако возвращаемый тип должен быть изменен до подходящего типа, чтобы поддерживать результат, который дает усреднение целых чисел.

Если вы посмотрите далее на Average, вы увидите следующее:

public static float Average(this IEnumerable<float> source);
public static float? Average(this IEnumerable<float?> source);

Снова вернемся к шаблону по умолчанию, возвращающему тот же тип, что и исходный входящий тип.

Теперь, когда мы увидим этот шаблон здесь, посмотрим, увидим ли мы это где-нибудь еще... давайте взглянем на System.Math, так как мы на этот вопрос.

Снова здесь мы видим тот же шаблон использования одного и того же типа возврата:

public static int Abs(int value);
public static long Abs(long value);

public static int Max(int val1, int val2);
public static long Max(long val1, long val2);

Я уже упоминаю об этом, это то, что составляет "ответ на вопрос". Я искал любые лучшие рекомендации MS или информацию о спецификации языка, которые могли бы намекнуть на то, что это языковой шаблон для MS для резервного копирования моего анализа, но я ничего не мог найти. При этом, если вы посмотрите на различные места в основных библиотеках .Net, особенно в пространстве имен System.Collections.Generic, вы увидите, что, если нет конкретной причины, тип возвращаемого значения соответствует типу коллекции.

Я не вижу причины отклонения этого правила от того, когда дело доходит до типов Nullable<T>.