タグ: C#

  • [C#,VB.NET]サポートされていないShift-JISエンコーディングのエラーとその解決方法

    はじめに

    .NET Coreや.NET 5+以降の環境で、Shift-JIS(日本語エンコーディング)を使用しようとすると、次のようなエラーが発生することがあります。

    System.ArgumentException: 'Shift-JIS' is not a supported encoding name.

    これは、.NET Coreや新しい.NETランタイムでは、Shift-JISなどの特定のエンコーディングがデフォルトではサポートされていないためです。しかし、CodePagesEncodingProviderを使用して、これらのエンコーディングを有効にすることができます。

    この記事では、C#とVB.NETでの解決方法を解説します。

    エラーの原因

    従来の.NET FrameworkではShift-JISエンコーディングがデフォルトでサポートされていましたが、.NET Coreや.NET 5+では軽量化とパフォーマンスの向上を目的に、デフォルトのエンコーディングサポートが制限されています。そのため、Shift-JISなどのコードページベースのエンコーディングを使用する場合は、明示的にサポートを有効にする必要があります。

    解決方法

    Step 1: CodePagesEncodingProviderの登録

    エンコーディングを有効にするために、アプリケーションの初期化時にCodePagesEncodingProviderを登録します。これにより、Shift-JISを含む多くのエンコーディングが利用可能になります。

    C#での実装

    C#では、次のようにしてShift-JISエンコーディングを有効にできます。

    using System;
    using System.Text;
    
    class Program
    {
        static void Main(string[] args)
        {
            // CodePagesEncodingProviderを登録してShift-JISを有効にする
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    
            // Shift-JISエンコーディングを取得
            Encoding encoding = Encoding.GetEncoding("Shift-JIS");
    
            // 以下でShift-JISエンコーディングが利用可能です。
        }
    }
    

    VB.NETでの実装

    VB.NETでは、同様にCodePagesEncodingProviderを登録することで、Shift-JISをサポートします。

    Imports System.Text
    
    Module Program
        Sub Main()
            ' Shift-JISをサポートするためのエンコーディングプロバイダを登録
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
    
            ' Shift-JISエンコーディングを取得
            Dim encoding As Encoding = Encoding.GetEncoding("Shift-JIS")
    
        ' 以下でShift-JISエンコーディングが利用可能です。
        End Sub
    End Module
    

    コードの説明

    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)

    この一行がポイントです。CodePagesEncodingProvider.InstanceEncoding.RegisterProviderに渡すことで、Shift-JISを含む追加のコードページベースのエンコーディングがサポートされるようになります。このメソッドを呼ばなければ、Shift-JISを使おうとした際に、ArgumentExceptionが発生します。

    Encoding.GetEncoding("Shift-JIS")

    GetEncodingメソッドでShift-JISエンコーディングを取得します。このメソッドはエンコーディングの名前を引数として受け取り、そのエンコーディングを使用するオブジェクトを返します。

    よくある質問 (FAQ)

    1. なぜShift-JISがデフォルトでサポートされていないのですか?

    .NET Coreや.NET 5+では、軽量かつ高速なランタイムを目指しており、一般的でないエンコーディングはデフォルトで省かれています。日本語環境でよく使われるShift-JISもその一部です。

    2. Encoding.GetEncodingで他のエンコーディングも使用できますか?

    はい、CodePagesEncodingProviderを登録すれば、Shift-JIS以外にもEUC-JPやISO-2022-JPなど、さまざまなエンコーディングが使用可能です。

    3. すべての.NETバージョンでこの方法は使えますか?

    この解決方法は、.NET Core 3.0以降および.NET 5+で利用可能です。従来の.NET Frameworkでは、特にエンコーディングプロバイダの登録は不要です。

    まとめ

    .NET Coreや.NET 5+では、Shift-JISエンコーディングを使用するためにCodePagesEncodingProviderの登録が必要です。C#やVB.NETでShift-JISを利用する場合、アプリケーションの初期化時にこのプロバイダを登録することで、スムーズにエンコーディングを使えるようになります。

    Shift-JISエンコーディングの対応に困っている方は、ぜひこの記事の方法を試してみてください。

  • [C#,.NET8]DataContextが使えない

    .NET Framework 4.8.1から.NETへ移行する際にDataContextが使えません。

    DataContext クラス (System.Data.Linq) | Microsoft Learn

    このため、.NETでは、別のORMを採用する必要があります。

    候補として挙がるORMは、

    • Entity Framework Core: .NETの標準的なORMとして広く利用されています。Entity Framework Coreは、リレーショナルデータベースとのやり取りを容易にし、LINQを利用してデータベースクエリを記述することができます。また、コードファーストやデータベースファーストのアプローチもサポートしています。
    • Dapper
    • NHibernate
    • Linq to DB

    です。

    個人的には、EF Coreが.NETの標準的なORMとして普及している点や、Linqを使って直感的に操作でき使いやすい点、アップデートも頻繁に行われている点で、お勧めです。

  • Intersect メソッドの活用法

    コレクション操作において、ある2つのリストや配列の共通要素を取得したい場合、C#の Intersect メソッドは非常に便利です。本記事では、Intersect メソッドの基本的な使い方から、実際の活用方法や注意点までを詳しく解説します。

    1. Intersect メソッドとは?

    Intersect メソッドは、2つのコレクションの共通部分を抽出するためのメソッドです。具体的には、あるリストや配列の要素のうち、もう一方のコレクションにも含まれている要素のみを返します。例えば、以下の2つのリストがあるとします。

    var list1 = new List<int> { 1, 2, 3, 4, 5 };
    var list2 = new List<int> { 3, 4, 5, 6, 7 };

    この2つのリストに対して Intersect メソッドを使うと、共通部分である {3, 4, 5} を取得できます。

    var intersection = list1.Intersect(list2);

    この結果、intersection には共通要素の列挙が含まれます。

    2. Intersect の使い方

    Intersect の基本的な使い方は非常にシンプルです。以下に典型的な使用例を示します。

    var list1 = new List<string> { "apple", "banana", "cherry" };
    var list2 = new List<string> { "banana", "cherry", "date" };
    
    var commonFruits = list1.Intersect(list2);
    
    foreach (var fruit in commonFruits)
    {
        Console.WriteLine(fruit);
    }

    このコードを実行すると、bananacherry が出力されます。

    3. Intersect の応用例

    実際のアプリケーションでは、Intersect は次のような場面で役立ちます。

    • データフィルタリング: 複数の条件に一致するデータセットを取得する際に使用できます。
    • 権限管理: ユーザーが複数のグループに属している場合、それらのグループが持つ共通の権限を取得する際に便利です。

    たとえば、次のようにユーザーの権限をチェックすることができます。

    var userRoles = new List<string> { "Admin", "Editor" };
    var requiredRoles = new List<string> { "Admin", "User" };
    
    var hasRequiredRole = userRoles.Intersect(requiredRoles).Any();
    
    if (hasRequiredRole)
    {
        Console.WriteLine("ユーザーは必要な権限を持っています。");
    }

    4. パフォーマンス

    Intersect メソッドはシンプルで強力ですが、大規模なデータセットに対して使用する場合には注意が必要です。内部的に HashSet を使用しているため、Intersect の計算量は一般的に O(n) です。これは効率的ですが、入力コレクションのサイズやデータの内容によってはパフォーマンスに影響を与える可能性があります。

    Intersect メソッドの計算量

    1. 初期化とセットアップ: 最初に、一方のコレクション(例えば list2)を HashSet に変換します。この変換には、リストの要素数に応じて O(n) の時間がかかります。
    2. Intersect 操作: 次に、もう一方のコレクション(例えば list1)の各要素に対して、HashSet 内にその要素が存在するかを確認します。この検索操作自体は O(1) ですが、これを list1 の全要素に対して行うため、最終的に O(m) の時間がかかります(ここで m は list1 の要素数)。

    これらを合わせると、Intersect メソッド全体の計算量は以下のようになります。

    • O(n) + O(m) = O(m + n)

    ここで、nlist2 の要素数、mlist1 の要素数です。

    runtime/src/libraries/System.Linq/src/System/Linq/Intersect.cs at main · dotnet/runtime · GitHub

    5.注意点

    特に、データの順序が重要な場合は、Intersect が順序を保持しないことに注意してください。順序を考慮する場合には、追加の処理が必要です。

    6. 結論

    Intersect メソッドは、コレクション操作において簡単に共通要素を取得できる便利なツールです。データのフィルタリングや権限管理など、さまざまな用途に応用できるため、ぜひ活用してみてください。

    効率的なコーディングと適切なデータ処理を行うためには、Intersect のようなメソッドを理解し、状況に応じて適切に使用することが重要です。データセットの規模や用途に応じた最適なアプローチを選択し、効果的なコーディングを実現しましょう。

  • [C#]Any関数の中にAny関数がある場合のリファクタリング

    あるリストの中に別のリストの要素が存在するかを確認したい場合、以下のようなコードを書くことができます。

    var flag = list.Any(x => list2.Any(y => y == x));

    しかし、Any関数の中にAny関数が入っており、可読性がかなり悪いです。ぱっと見は何をやっているのか全く分かりません。あと別の記事で書いていますが、かなり遅いです。

    [C#]Any関数の速度 – TOMATONOTE (tomato-note.com)

    今回は備忘録的に修正案を三つ提示します。

    Any の中で Contains を使用する

    Any の中で Contains メソッドを使うことで、より簡潔なコードにできます。O(n)で探索してくれます。

    var flag = list.Any(x => list2.Contains(x));

    Intersect を使用する

    Intersect メソッドを使うことで、両リストの共通要素が存在するかどうかをチェックできます。Any を使って共通要素が1つでもあるか確認します。

    var flag = list.Intersect(list2).Any();

    HashSet を使用して効率化

    ToHashSetの計算量が無視できるくらいlist2 が大きい場合、HashSet を使うことで、Contains 操作を高速化できます。

    var set = list2.ToHashSet();
    var flag = list.Any(x => set.Contains(x));
  • [C#]Any関数の速度

    ChatGPT大先生にとあるAny関数を使ったソースコードを添削していただくと、大体HashSetにしたほうがO(1)だから、こっちの方がいいよと仰います。

    本当なのかいろいろと試してみたいと思います。

    素のままの検索速度の差

    順序がランダムな10000の数字の配列を用意します。その中から0を探すことを1000回行ったときの速度を計測しました。

    Any関数の場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = list.Any(x => x == 0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:43ms
    処理時間:41ms
    処理時間:41ms
    処理時間:41ms
    処理時間:42ms
    処理時間:17ms
    処理時間:14ms
    処理時間:14ms
    処理時間:13ms
    処理時間:13ms

    HashSetの場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var hashSet = list.ToHashSet();
        var flag = hashSet.Contains(0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:131ms
    処理時間:121ms
    処理時間:110ms
    処理時間:123ms
    処理時間:169ms
    処理時間:114ms
    処理時間:120ms
    処理時間:153ms
    処理時間:143ms
    処理時間:134ms

    個人的には既に意外ですが、まさかのAny関数の勝利でした。

    毎回ToHashSetする方がパフォーマンスとしては悪いという結果になりました。

    ちなみにToHashSet()をタイマーの外で行うと、さすがO(1) の実行速度です。

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var hashSet = list.ToHashSet();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = hashSet.Contains(0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms

    配列の中に別の配列の要素が含まれるか

    とある大きい配列の中に、別で用意した配列が含まれているかのチェックをよくやることがあり、これをChatGPT先生に聞くと、HashSetを使ったほうが良いという回答をよく貰います。

    文章だと分かりづらいので、例をソースコードで示すと、下記のようになります。

    var list1 = [0,1,2,3,4,5,6,7,8,9];
    var list2 = [2,5];
    // 自分が提案した方法
    var list3 = list1.Where(x => list2.Any(y => x == y));
    
    // ChatGPT先生のリファクタリング結果
    var hashSet = list2.ToHashSet();
    var list4 = list1.Where(x => hashSet.Contains(x));

    実際の実務で使うコードは、オブジェクトの配列となるため、これよりも複雑ですが、大雑把に言えば、このようになります。もちろん配列の大きさ、検索するオブジェクトの位置によって実行速度は大きく変わってきますが、ここでは、大量のオブジェクトの中を検索するという前提で考えていきます。

    Any関数を使う場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var list2 = Enumerable.Range(0,1000);
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = list.Any(x => list2.Any(y => y == x));
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:182ms
    処理時間:174ms
    処理時間:172ms
    処理時間:171ms
    処理時間:181ms
    処理時間:176ms
    処理時間:175ms
    処理時間:176ms
    処理時間:181ms
    処理時間:190ms

    Any関数が二つあって一見、何をやってるのか非常に分かりにくいので、出来れば、これは避けたいです。

    HashSetを使う場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var list2 = Enumerable.Range(0,1000);
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var hash = list2.ToHashSet();
        var flag = list.Any(x => hash.Contains(x));
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:12ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:6ms
    処理時間:6ms
    処理時間:8ms
    処理時間:10ms

    !!

    list2が大きい場合、list2をHashSetにする方が速くなりました。

  • [C#, VB.NET] 警告CA1416の意味

    CA1416は、.NETにおけるプラットフォームの互換性に関連する警告メッセージです。この警告は、コードが特定のプラットフォーム(例えば、Windows、Linux、macOSなど)でのみ実行される可能性があることを示しています。

    CA1416 警告の意味

    この警告は、あなたが書いたコードが現在実行中のプラットフォームで動作しない可能性があることを知らせるためのものです。たとえば、Windows固有のAPIを使用しているコードをクロスプラットフォームアプリケーションに含めると、この警告が表示されることがあります。

    対策方法

    条件付きコンパイルディレクティブを使用する

    特定のプラットフォームでのみ実行されるコードを囲むために、条件付きコンパイルディレクティブを使用できます。

    #if WINDOWS
    // Windows固有のコード
    #endif

    プラットフォームのチェック

    実行時にプラットフォームをチェックし、対応するコードを実行することもできます。

    if (OperatingSystem.IsWindows())
    {
        // Windows固有のコード
    }
    else if (OperatingSystem.IsLinux())
    {
        // Linux固有のコード
    }

    プラットフォーム互換性属性を使用する

    特定のプラットフォームでのみ利用可能なAPIを示すために、プラットフォーム互換性属性を使用できます。

    [SupportedOSPlatform("windows")]
    public void WindowsSpecificMethod()
    {
        // Windows固有のコード
    }

    プラットフォーム互換性の確認

    .NET 5以降では、プラットフォーム互換性を確認するための新しいAPIが導入されました。これにより、特定のプラットフォームでのみ使用可能なAPIや機能を明示的に指定できます。これにより、開発者はコードが異なるプラットフォームでどのように動作するかをより正確に制御できます。

    まとめ

    CA1416警告は、クロスプラットフォームアプリケーションの開発者がコードの互換性を確保するために重要な指針となります。この警告を無視せず、適切な対策を講じることで、アプリケーションの信頼性と移植性を向上させることができます。

  • [C#,VB.NET]警告CA2200の意味

    CA2200は、キャッチされた例外を再スローする際にスタックトレース情報が失われることに関する警告です。この警告は、例外をキャッチした後に単純に再スローするのではなく、元の例外情報を保持するための適切な方法を使用するよう促すものです。

    CA2200 警告の意味

    通常、例外がキャッチされた後に再スローされると、例外のスタックトレース情報がリセットされ、新しい例外として扱われます。これにより、元の例外がどこで発生したのかを特定するのが難しくなります。この警告は、この問題を避けるために、元の例外情報を保持したまま再スローする方法を使用することを推奨しています。

    対策方法

    例外をキャッチした後で再スローする際には、次のように単にthrowキーワードを使用します。これにより、元の例外とそのスタックトレース情報が保持されます。

    try
    {
        // 例外を引き起こす可能性のあるコード
    }
    catch (Exception ex)
    {
        // 例外を処理するためのコード
        throw;  // 元の例外を再スロー
    }

    不適切な再スローの例

    以下は、警告CA2200が発生する典型的な例です。ここでは、新しい例外がスローされるため、元の例外情報が失われます。

    try
    {
        // 例外を引き起こす可能性のあるコード
    }
    catch (Exception ex)
    {
        // 例外を処理するためのコード
        throw ex;  // これは元の例外を再スローするのではなく、新しい例外として扱われます
    }

    正しい再スローの方法

    上述の通り、単にthrowを使用することで、元の例外情報を保持したまま再スローできます。

    try
    {
        // 例外を引き起こす可能性のあるコード
    }
    catch (Exception ex)
    {
        // 例外を処理するためのコード
        throw;  // 元の例外を再スロー
    }

    まとめ

    CA2200警告は、例外の再スロー時に元のスタックトレース情報を保持するための適切な方法を使用することを促すものです。例外をキャッチして再スローする際には、必ずthrowキーワードを使用して元の例外情報を保持するようにしましょう。これにより、デバッグや問題のトラブルシューティングが容易になります。

  • [C#]拡張メソッドの使い所

    拡張メソッドは、C#で既存のクラスに新しいメソッドを追加するための強力なツールです。この記事では、拡張メソッドの作り方、メリット、デメリットについて詳しく解説します。

    拡張メソッドの作り方

    自作クラスに拡張メソッドを追加

    まず、自作のクラスに拡張メソッドを追加する方法を見てみましょう。以下に、MyClass というクラスに対して拡張メソッドを追加する例を示します。

    public class MyClass
    {
        public int Value { get; set; }
    }
    
    public static class MyClassExtensions
    {
        public static void PrintValue(this MyClass myClass)
        {
            Console.WriteLine(myClass.Value);
        }
    
        public static void AddToValue(this MyClass myClass, int addValue)
        {
            myClass.Value += addValue;
        }
    }

    拡張メソッドは静的クラスを定義することで作成可能です。このクラスの中にメソッドを定義します。メソッドの第一引数にthis MyClass myClassを入れることでMyClassの拡張メソッドを定義できます。引数をつけたい場合は、第二引数を定義することで引数ありの拡張メソッドを定義できます。

    MyClassExtensionsは、任意の名前に出来ますが、可読性の観点から「〇〇Extensions」とすることをおすすめします。

    この拡張メソッドを使用することで、MyClass のインスタンスで PrintValue メソッドを呼び出すことができます。

    MyClass instance = new MyClass { Value = 42 };
    instance.PrintValue();  // 出力: 42
    instance.AddValue(10); // 52

    標準ライブラリのクラスに拡張メソッドを追加

    次に、標準ライブラリのクラスに対して拡張メソッドを追加する方法を見てみましょう。自作クラスに普通にメソッドを定義すれば良いので拡張メソッドを定義することは、あまりないと思います。拡張メソッドは、標準ライブラリのクラスなどに自作のメソッドを追加したい場合によく利用されます。以下に、string クラスに対して拡張メソッドを追加する例を示します。

    public static class StringExtensions
    {
        public static bool IsNullOrEmpty(this string str)
        {
            return string.IsNullOrEmpty(str);
        }
    }

    この拡張メソッドを使用することで、string のインスタンスで IsNullOrEmpty メソッドを呼び出すことができます。

    string myString = null;
    bool result = myString.IsNullOrEmpty();  // true

    拡張メソッドのメリット

    継承をしなくても、クラスにメソッドを追加できる

    拡張メソッドの最大のメリットは、既存のクラスに対して継承を使わずにメソッドを追加できることです。これにより、クラスの継承階層をシンプルに保つことができます。標準ライブラリにメソッドを追加したいために継承を利用して、新しい親クラスを定義すると可読性、保守性が低下します。

    利用箇所での可読性の向上

    拡張メソッドを使うことで、メソッドチェーンを作成したり、自然な文脈でメソッドを呼び出すことができます。これにより、コードの可読性が向上し、直感的に理解しやすくなります。

    if (myString.IsNullOrEmpty())
    {
        // 何かの処理
    }

    拡張メソッドのデメリット

    プロジェクトに新規加入した開発者にとっては可読性が低下

    拡張メソッドは、その存在を知っている開発者にとっては便利ですが、新しくプロジェクトに参加した開発者にとっては、どのクラスがどのメソッドを持っているのかが分かりにくくなる可能性があります。実装されているクラスも静的クラスで、名前も「〇〇Extensions」です。これにより、コードの理解が難しくなることがあります。

    まとめ

    拡張メソッドは、既存のクラスに対して新しいメソッドを追加するための便利なツールですが、その使用には注意が必要です。適切に利用することで、コードの再利用性や可読性を向上させることができますが、過度な使用はプロジェクトの可読性を損なう可能性があります。拡張メソッドの利点と欠点を理解し、バランスの取れた方法で利用することが重要です。

    最近まで知らなかったのですが、Enum型にも拡張メソッドを定義できます。これに関しては、可読性を上げることができてかなりいいと感じています。

  • [C#]ランダムな配列の作り方

    検証用として考えました。(実務では、無意味ですね)

    ある配列があって中身をランダムな配列にするやり方をまとめます。

    Guidを利用する方法

    var randomList = list.OrderBy(i=>Guid.NewGuid()).ToArray();

    Guidは、生成のたびに異なる値を出してくれます。これを利用して、Guidの順に並び替えることで任意の配列をランダムな順序に変更できます。

    例えば、数値のランダムな順序の配列なら

    var list = Enumerable.Range(0,10);
    var randomList = list.OrderBy(i=>Guid.NewGuid()).ToArray();
    foreach(var i in randomList)
    {
        Console.WriteLine(i);
    }

    このようにすれば、0~9までの間のランダムな配列が簡単に手に入ります。

    またlistを文字列の配列にすれば、ランダムな順序の文字列の配列も作ることができます。

    var list = new List<string>{"apple", "banana", "hogehoge","hugahuga", "orange"};
    var randomList = list.OrderBy(i=>Guid.NewGuid()).ToArray();
    foreach(var i in randomList)
    {
        Console.WriteLine(i);
    }

    次回は、これを使って検証したいことがあったので、それについて書いていきます!

  • [C#]実行時間計測

    polyglotで実行時間を計測したいと思って、調べました。

    Stopwatchクラスを使えば、簡単に計測できます。

    using System.Diagnostics;
    
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,10))
    {
        // 何らかの処理
        await Task.Delay(1000);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");

    StartNew()は、静的メソッドでStopwatchクラスのインスタンスを作成後、すぐにStopwatchをスタートします。

    もちろん下記で書いても同じ動作です。

    using System.Diagnostics;
    
    var stopwatch = new Stopwatch()
    stopwatch.Start();
    foreach(var i in Enumerable.Range(0,10))
    {
        // 何らかの処理
        await Task.Delay(1000);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");