Enable code quality rule CA1851
The PR enables the .NET code quality rule CA1851: Possible multiple enumerations of IEnumerable collection and fixes the violations in the codebase. This closes https://github.com/Azure/bicep/issues/13521.
Some key learnings:
Avoid if (enumerable.Any())
The practice of using if (enumerable.Any()) { ...enumerable.XXX() } to determine if an IEnumerable is not empty before proceeding with more operations is common in our code base, but it should be avoided. The reason is that Any() might execute an expensive filtering operation, particularly when used with a Where clause. Although Any() is designed to terminate as soon as an item is detected, thus may not iterating through the entire collection, it can still have performance impacts. Besides, this approach is not free from side effects.
There are several ways to address this:
Replace Any() + First() with FirstOrDefault()
- Before
if (enumerable.Any())
{
DoSomethingWith(enumerable.First());
}
- After
if (enumerable.FirstOrDefault() is { } first)
{
DoSomethingWith(first);
}
Perform the empty check on a more specific collection type
- Before
if (!enumerable.Any())
{
return;
}
var array = enumerable.ToArray();
- After
// Utilizing ToArray will yield Array.Empty<T>() in the case of an empty collection, hence no allocation is made.
var array = enumerable.ToArray();
if (enumerable.Length == 0)
{
return;
}
Bypass Any() through the use of a Boolean variable
- Before
foreach (var item in enumerable)
{
...
}
if (enumerable.Any())
{
...
}
- After
var hasItem = false;
foreach (var item in enumerable)
{
hasItem = true;
...
}
if (hasItem)
{
...
}
Use DefaultIfNotEmpty()
- Before
var max = enumerable.Any() ? enumerable.Max() : 0;
- After
var max = enumerable.DefaultIfEmpty(0).Max();
Addressing Intentional Multiple Enumeration
In scenarios where repeated enumeration of a collection is intentional, alternative strategies can mitigate the direct enumeration of an IEnumerable.
Favor Specific Interfaces like IReadOnlyCollection<T> or IReadOnlyList<T>
- Before
private void Foo(IEnumerable<int> numbers)
{
var oddNumbers = numbers.Select(x => x % 2 != 0).ToArray();
var evenNumbers = numbers.Select(x => x % 2 == 0).ToArray();
}
- After
private void Foo(IReadOnlyList<int> numbers)
{
var oddNumbers = numbers.Select(x => x % 2 != 0).ToArray();
var evenNumbers = numbers.Select(x => x % 2 == 0).ToArray();
}
Convert to Array or List When the IEnumerable parameter Type Cannot Be Altered
- Before
private void Foo(IEnumerable<int> numbers)
{
var oddNumbers = numbers.Select(x => x % 2 != 0).ToArray();
var evenNumbers = numbers.Select(x => x % 2 == 0).ToArray();
}
- After
private void Foo(IEnumerable<int> numbers)
{
var numbersArray = numbers.ToArray();
var oddNumbers = numbersArray .Select(x => x % 2 != 0).ToArray();
var evenNumbers = numbersArray .Select(x => x % 2 == 0).ToArray();
}
Test this change out locally with the following install scripts (Action run 8193199706)
VSCode
- Mac/Linux
bash <(curl -Ls https://aka.ms/bicep/nightly-vsix.sh) --run-id 8193199706 - Windows
iex "& { $(irm https://aka.ms/bicep/nightly-vsix.ps1) } -RunId 8193199706"
Azure CLI
- Mac/Linux
bash <(curl -Ls https://aka.ms/bicep/nightly-cli.sh) --run-id 8193199706 - Windows
iex "& { $(irm https://aka.ms/bicep/nightly-cli.ps1) } -RunId 8193199706"
Test Results
65 files - 34 65 suites - 34 22m 11s :stopwatch: - 18m 43s 10 677 tests - 20 10 675 :white_check_mark: - 20 2 :zzz: ±0 0 :x: ±0 25 244 runs - 12 618 25 240 :white_check_mark: - 12 616 4 :zzz: - 2 0 :x: ±0
Results for commit 5c04c6e2. ± Comparison against base commit 9fd452ed.
:recycle: This comment has been updated with latest results.