【C#】Anyメソッド解説 & 範囲指定できるように拡張する

LinqのAnyメソッド使い方と範囲指定できるように機能拡張を行います。

Anyメソッドとは

簡単な説明

まず初めにAnyメソッドの説明です。

AnyメソッドはIEnumerableの拡張メソッド(=Linq)として定義されていて指定した配列やリストに中身があるかどうかを判定できます。

// Anyの宣言(1):中身があるかどうか判定できる
public static bool Any<TSource>(this IEnumerable<TSource> source);
// Anyの宣言(2):predicateで指定した条件が中身に存在するか確認できる
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Anyメソッドの使い方

使い方は以下の通りです。

// Anyの宣言(1)の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source);

public void Foo()
{
    // リストに中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any();
    // > result_1 = false : 空のリストの場合 false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any();
    // > result_2 = true : 中身があれば true
}

// Anyの宣言(2):の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

public void Foo()
{
    // リストに条件に一致する中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_1 = false : 条件を指定しても空なので false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_2 = true : 5, 6が条件に一致するため true, p > 10 とすれば false
}

サンプルではListに対してAnyメソッドを実行していますが配列に対しても同様の操作が可能です。

更に、IEnumerableを継承しているほかの型でもAnyで確認ができます。

Anyメソッドは使用頻度(低)

余談ですがAnyメソッドは単体では使う機会はほぼ無いです。要素があるかどうかは「Count」や「Length」で確認できます。配列とListで操作を統一できるメリットはありますが、内部でイテレータを使用しているため速度がCountやLengthよりかなり遅いです。

それに条件を指定するほうも、同じLinqでContainsと機能がほぼ被っているのでメソッド名から意図がくみ取りやすいContainsを使用します。最近見かけませんが、昔に Linq to Object or SQL という構文中で割と使っていましたが最近ほぼ使わないですね。

Anyに範囲を指定するように拡張する

タイトルの件ですが、そんなAnyメソッドですが利用性を向上するために範囲指定できるようにしたいと思います。

EnumerableExtensionクラス

以下の通りEnumerableExtensionクラスを作成してIEnumerableの拡張メソッドとして処理を作成したいと思います。

// IEnumerableの拡張メソッドを定義するクラス
public static class EnumerableExtension
{
    // (1) 要素の開始位置を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len)
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"inde={startIndex}");
        
        int i = 0;
        foreach (var item in source)
        {
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }

    // (2) 要素の対象範囲を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex, int endIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len) 
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"nameof(startIndex)={startIndex}");
        if (endIndex < 0 || endIndex > len)
            throw new ArgumentOutOfRangeException(nameof(endIndex), $"nameof(endIndex)={endIndex}");

        int i = 0;
        foreach (var item in source)
        {
            if (i > endIndex) return false;
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }
}

指定できる範囲の指定は配列の要素番号で行います。

使い方

上記メソッドの使い方はそれぞれ以下の通りです。

拡張メソッドとして作成したためAnyと同じようにListに対してメソッド呼び出しで処理を記述できます。

public void Foo(params string[] args)
{
    // (1) 要素の開始位置を指定して要素の有無を確認する
    var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_1 = list.Any(p => p > 5, 3); // リストの3つ目以降に5以上の値があるか?
    Console.WriteLine(result_1);
    // result_1 = true : p > 10 等にすると存在しないのでfalse
    
    // (2) 要素の対象範囲を指定して要素の有無を確認する
    bool result_2 = list.Any(p => p == 3, 2, 3); // リストの2つ目 ~ 4つ目の範囲内に3が存在するか?
    Console.WriteLine(result_2);
    // result_2 = true : p == 10 等にすると存在しないのでfalse
}

【参考】Anyメソッドの実装

.NET(Framework)のソースコードはReferencesourceというサイトで中身が確認できます。

今回紹介したAnyメソッドの実装は以下の通りです。

https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,8788153112b7ffd0

public static bool Any<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) return true;
    }
    return false;
}

public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) {
        if (predicate(element)) return true;
    }
    return false;
}

見ればわかると思いますが、Any()はイテレータで最初の要素が取れればtrueで要素があると判定しています。

あんまり効率が良くなさそうです。

Any(predicate)は最初から最後までforeachでループして要素があるか確認してます。IEnumerableは長さが無限の場合が稀にあるので安易に実行すると処理が戻ってこない可能性があります。まぁ滅多に無いので頭の片隅に置いておくくらいで大丈夫です。