روش بهینه‌ی بررسی خالی بودن مجموعه‌ها و آرایه‌ها در NET 5.0.
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: شش دقیقه

پیشتر مطلب «Count یا Any » را در این سایت مطالعه کرده‌اید که در پایان آن این نتیجه گیری صورت گرفته‌است:
«از این پس حین استفاده از انواع و اقسام لیست‌ها، آرایه‌ها، IEnumerable‌ها و امثال آن‌ها، جهت بررسی خالی بودن یا نبودن آن‌ها تنها از متد Any فراهم شده توسط LINQ استفاده نمائید.»

اکنون پس از سال‌ها، قصد داریم صحت این مساله را با NET 5.0. بررسی کنیم که آیا هنوز هم متد Any، بهترین متد بررسی خالی بودن مجموعه‌ها و آرایه‌های NET 5.0. است یا خیر؟


نحوه‌ی بررسی کارآیی روش‌های مختلف خالی بودن مجموعه‌ها و آرایه‌ها در C# 9.0

در ابتدا یک لیست، یک Enumerable و یک آرایه را به صورت زیر مقدار دهی می‌کنیم و هر سه‌ی این‌ها می‌توانند نال هم باشند:
private IList<int>? _idsList;
private IEnumerable<int>? _idsEnumerable;
private int[]? _idsArray;

[GlobalSetup]
public void Setup()
{
    _idsEnumerable = Enumerable.Range(0, 10000);
    _idsList = _idsEnumerable.ToList();
    _idsArray = _idsEnumerable.ToArray();
}

اکنون که C# 9.0 در اختیار ما است به همراه pattern matching و همچنین Null Conditional Operator و غیره، می‌توان روش‌های زیر را برای بررسی خالی بودن این مجموعه‌ها و آرایه‌ها بکار گرفت:
1- استفاده از Null coalescing برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
var list = _idsList ?? new List<int>();
if (list.Any() is false) { }

2- استفاده از pattern matching برای بررسی نال بودن مجموعه و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList is null || _idsList.Any() is false) { }

3- استفاده از روش سنتی مقایسه‌ی مستقیم با null و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList == null || _idsList.Any() is false) { }

4- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از متد Any برای بررسی خالی بودن آن:
if (_idsList?.Any() is false) { }

5- استفاده از pattern matching برای بررسی مقدار خاصیت Count یک لیست یا آرایه. البته Enumerable‌ها به همراه این خاصیت نیستند و یا باید آن‌ها را به لیست و یا آرایه تبدیل کرد و یا می‌توان متد ()Count آن‌ها را فراخوانی نمود:
if (_idsList is { Count: > 0 } is false) { }

6- استفاده از Null Conditional Operator برای بررسی نال بودن و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
if (_idsList?.Count == 0) { }

7- استفاده از روش سنتی مقایسه‌ی مستقیم با null و سپس استفاده از مقدار خاصیت Count لیست، برای بررسی خالی بودن آن:
if (_idsList == null || _idsList.Count == 0) { }


کدهای کامل این بررسی به صورت زیر هستند: AnyCountBenchmark.zip

ابتدا ارجاعی به BenchmarkDotNet به برنامه اضافه شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
  </ItemGroup>
</Project>

و سپس کدهای زیر، بررسی کارآیی روش‌های مختلف تعیین خالی بودن مجموعه‌ها را انجام می‌دهند:
using BenchmarkDotNet.Running;

namespace AnyCountBenchmark
{
    public static class Program
    {
        static void Main(string[] args)
        {
#if DEBUG
            System.Console.WriteLine("Please set the project's configuration to Release mode first.");
#else
            BenchmarkRunner.Run<Scenarios>();
#endif
        }
    }
}

به همراه سناریوهای مختلف زیر:
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;

namespace AnyCountBenchmark
{
    [SimpleJob(RuntimeMoniker.NetCoreApp50)]
    [Orderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)]
    [RankColumn]
    public class Scenarios
    {
        private IList<int>? _idsList;
        private IEnumerable<int>? _idsEnumerable;
        private int[]? _idsArray;

        [GlobalSetup]
        public void Setup()
        {
            _idsEnumerable = Enumerable.Range(0, 10000);
            _idsList = _idsEnumerable.ToList();
            _idsArray = _idsEnumerable.ToArray();
        }

        #region Any_With_Null_coalescing
        [Benchmark]
        public void List_Any_With_Null_coalescing()
        {
            var list = _idsList ?? new List<int>();
            if (list.Any() is false) { }
        }

        [Benchmark]
        public void Array_Any_With_Null_coalescing()
        {
            var array = _idsArray ?? Array.Empty<int>();
            if (array.Any() is false) { }
        }

        [Benchmark]
        public void Enumerable_Any_With_Null_coalescing()
        {
            var enumerable = _idsEnumerable ?? Enumerable.Empty<int>();
            if (enumerable.Any() is false) { }
        }
        #endregion

        #region Any_With_Is_Null_Check
        [Benchmark]
        public void List_Any_With_Is_Null_Check()
        {
            if (_idsList is null || _idsList.Any() is false) { }
        }

        [Benchmark]
        public void Array_Any_With_Is_Null_Check()
        {
            if (_idsArray is null || _idsArray.Any() is false) { }
        }

        [Benchmark]
        public void Enumerable_Any_With_Is_Null_Check()
        {
            if (_idsEnumerable is null || _idsEnumerable.Any() is false) { }
        }
        #endregion

        #region Any_Any_With_Null_Equality_Check
        [Benchmark]
        public void List_Any_With_Null_Equality_Check()
        {
            if (_idsList == null || _idsList.Any() is false) { }
        }

        [Benchmark]
        public void Array_Any_With_Null_Equality_Check()
        {
            if (_idsArray == null || _idsArray.Any() is false) { }
        }

        [Benchmark]
        public void Enumerable_Any_With_Null_Equality_Check()
        {
            if (_idsEnumerable == null || _idsEnumerable.Any() is false) { }
        }
        #endregion

        #region Any_With_Null_Conditional_Operator
        [Benchmark]
        public void List_Any_With_Null_Conditional_Operator()
        {
            if (_idsList?.Any() is false) { }
        }

        [Benchmark]
        public void Array_Any_With_Null_Conditional_Operator()
        {
            if (_idsArray?.Any() is false) { }
        }

        [Benchmark]
        public void Enumerable_Any_With_Null_Conditional_Operator()
        {
            if (_idsEnumerable?.Any() is false) { }
        }
        #endregion

        #region Count_With_Pattern_Matching
        [Benchmark]
        public void List_Count_With_Pattern_Matching()
        {
            if (_idsList is { Count: > 0 } is false) { }
        }

        [Benchmark]
        public void Array_Length_With_Pattern_Matching()
        {
            if (_idsArray is { Length: > 0 } is false) { }
        }

        [Benchmark]
        public void Enumerable_Count_With_Pattern_Matching()
        {
            var list = _idsEnumerable?.ToList();
            if (list is { Count: > 0 } is false) { }
        }
        #endregion

        #region Count_With_Null_Conditional_Operator
        [Benchmark]
        public void List_Count_With_Null_Conditional_Operator()
        {
            if (_idsList?.Count == 0) { }
        }

        [Benchmark]
        public void Array_Length_With_Null_Conditional_Operator()
        {
            if (_idsArray?.Length == 0) { }
        }

        [Benchmark]
        public void Enumerable_Count_With_Null_Conditional_Operator()
        {
            if (_idsEnumerable?.Count() == 0) { }
        }
        #endregion

        #region Count_With_Null_Equality_Check
        [Benchmark]
        public void List_Count_With_Null_Equality_Check()
        {
            if (_idsList == null || _idsList.Count == 0) { }
        }

        [Benchmark]
        public void Array_Length_With_Null_Equality_Check()
        {
            if (_idsArray == null || _idsArray.Length == 0) { }
        }

        [Benchmark]
        public void Enumerable_Count_With_Null_Equality_Check()
        {
            if (_idsEnumerable == null || _idsEnumerable.Count() == 0) { }
        }
        #endregion
    }
}
یکبار اجرای آن، نتیجه‌ی زیر را به همراه داشت:


نتایج حاصل:

- بررسی خالی بودن آرایه‌ها، بسیار سریعتر از بررسی خالی بودن لیست‌ها و این مورد نیز سریعتر از Enumerable‌ها است.
- اگر از آرایه‌ها و یا لیست‌ها استفاده می‌کنید، بررسی خاصیت Length و یا خاصیت Count آن‌ها، بسیار سریعتر از بکارگیری متد Any بر روی آن‌ها است.
- اگر از Enumerableها استفاده می‌کنید، استفاده از متد Any بر روی آن‌ها، بسیار سریعتر از بکارگیری متد ()Count و یا تبدیل آن‌ها به لیست و سپس بررسی خاصیت Count آن‌ها است.
- بررسی نال بودن با pattern matching یا همان is null، نسبت به روشی سنتی استفاده‌ی از null ==، سریعتر است. علت آن‌را در مطلب «روش ترجیح داده شده‌ی مقایسه مقادیر اشیاء با null از زمان C# 7.0 به بعد» می‌توانید مطالعه کنید.

بنابراین برای بررسی خالی بودن آرایه‌ها و لیست‌ها، بهتر است از خاصیت Length و یا Count آن‌ها استفاده کرد و برای Enumerableها از متد ()Any.
  • #
    ‫۲ سال و ۶ ماه قبل، جمعه ۲۹ بهمن ۱۴۰۰، ساعت ۱۶:۰۹
    یک نکته‌ی تکمیلی: اضافه شدن متد TryGetNonEnumeratedCount به دات نت 6
    public static bool TryGetNonEnumeratedCount(this IEnumerable<T> source, out int count);

    زمانیکه متد ()Count را بر روی یک <IEnumerable<T فراخوانی می‌کنیم، دو کار ممکن رخ می‌دهند:
    الف) کتابخانه‌ی LINQ سعی می‌کند تا این نوع را به یکی از انواع مشتق شده‌ی از IEnumerable که دارای خاصیت Count است (مانند IList و امثال آن)، تبدیل کند تا بر این اساس بتواند به سرعت تعداد عناصر آن‌را تشخیص دهد.
    ب) اگر حالت الف میسر نشد، تمام عناصر IEnumerable را جهت یافتن تعداد آن‌ها، پیمایش و شمارش می‌کند که این عملیات برای مجموعه‌های بزرگ می‌تواند بسیار زمانبر باشد.

    اکنون در دات نت 6، حالت امن گزینه‌ی الف، با ارائه‌ی متد الحاقی جدید TryGetNonEnumeratedCount، میسر شده‌است. یعنی اگر مجموعه‌ی مدنظر به همراه خاصیت Count بود، بلافاصله از آن استفاده کرده و خروجی out آن‌را ارائه می‌دهد، در غیراینصورت false را بر می‌گرداند و مقدار count صفر خواهد بود؛ یعنی حالت ب را دیگر هیچگاه اجرا نخواهد کرد و به این ترتیب می‌توان به کنترل بیشتری بر روی API ارائه شده رسید.
    if (movies.TryGetNonEnumeratedCount(out int count))
    {
        Console.WriteLine($"The count is {count}");
    }
    else
    {
        Console.WriteLine("Could not get a count of movies without enumerating the collection");
    }