No Unchecked Enumerable Sum

ID

csharp.no_unchecked_enumerable_sum

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

arithmetic-overflow, linq, misleading-code, reliability

Description

Reports a Sum() call over a sequence of int or long that sits inside an unchecked block or unchecked(…​) expression. The aggregate adds with overflow checking of its own, so the unchecked wrapper changes nothing about how it behaves.

Rationale

unchecked is a statement of intent. It says the arithmetic inside may overflow, that wrapping is the accepted outcome, and that no exception should be raised. Around ordinary + that is exactly what happens.

The LINQ Sum aggregate does not participate. Its implementation adds inside its own checked context, so it raises an overflow exception when the running total leaves the range of the element type — whether or not the call site is unchecked. The wrapper is not merely redundant: it is a comment that contradicts the code it encloses.

The cost is paid twice. A reader looking for the code paths that can throw skips this one, because it is marked as arithmetic that wraps. And when the exception does arrive, it comes from inside a framework method, so the stack trace names the aggregate rather than the unchecked that was supposed to be governing it — and the enclosing block is the last place anyone looks.

double, float and decimal sequences are not reported. Floating-point arithmetic has no checked form at all, and the decimal aggregate throws on overflow in either context, so unchecked was never promising anything there.

A Sum inside a try that has a catch is also not reported: catching the overflow is a decision about what the code should do when the total does not fit, which makes the surrounding unchecked redundant rather than misleading.

using System;
using System.Collections.Generic;
using System.Linq;

public class Totals
{
    public int Wrapped(int[] values)
    {
        unchecked
        {
            return values.Sum();            // FLAW — throws on overflow regardless
        }
    }

    public double Average(List<double> samples)
    {
        unchecked
        {
            return samples.Sum();           // OK — floating point has no checked form
        }
    }

    public int Guarded(int[] values)
    {
        unchecked
        {
            try
            {
                return values.Sum();        // OK — the overflow is handled deliberately
            }
            catch (OverflowException)
            {
                return int.MaxValue;
            }
        }
    }
}

Remediation

Decide what should happen when the total does not fit, and write that.

To let the total wrap, do the addition yourself inside the unchecked block with a fold or a loop, so the context you declared is the one doing the arithmetic:

unchecked
{
    int total = 0;
    foreach (int value in values) total += value;
    return total;
}

To keep the exception, drop the unchecked wrapper — it was never having an effect — and handle OverflowException where the failure can be dealt with.

To avoid the overflow instead of reacting to it, widen the accumulator: sum into a long with values.Sum(v ⇒ (long)v), or into a decimal when even long could be exceeded.