CS801~2000

C#の非同期処理におけるCS1986エラーの原因と対策について解説

C#の非同期処理でawaitを使用する際、対象の型に静的ではなくインスタンスとして定義されたGetAwaiterメソッドが必要です。

GetAwaiterはINotifyCompletionインターフェイスを実装したオブジェクトを返すことが求められ、この条件を満たさない場合にCS1986エラーが発生します。

CS1986エラーの発生原因

awaitキーワードが正しく動作するためには、対象の型に対して特定の非静的なメソッドが存在する必要があります。

ここでは、その仕組みと必要な条件について解説します。

awaitの仕組みと必要条件

awaitキーワードの動作原理

C#の非同期処理で使用されるawaitキーワードは、非同期メソッド内で指定したオブジェクトの完了を待機する役割を果たします。

コンパイラはawaitが使用されている箇所に対して以下の手順を自動生成します。

  • 指定されたオブジェクトに対してGetAwaiter()メソッドを呼び出す。
  • 返されたオブジェクトのIsCompletedプロパティを確認し、処理が完了しているかを判定する。
  • 完了していなければ、OnCompleted(Action continuation)またはUnsafeOnCompleted(Action continuation)を呼び出し、待機状態を管理する。
  • 完了後にGetResult()メソッドを呼び出して、結果を取得する。

この一連の動作により、非同期処理の実行がシンプルに記述できます。

内部で状態マシンが生成されるため、プログラマは複雑な待機処理を記述する必要がありません。

GetAwaiterメソッドが求める仕様

awaitの対象となる型は、非静的なGetAwaiter()メソッドを実装する必要があります。

このメソッドが返すオブジェクトは、以下の要件を満たす必要があります。

  • インターフェイスINotifyCompletion、もしくは必要に応じてICriticalNotifyCompletionを実装していること。
  • bool IsCompleted { get; }プロパティが存在すること。
  • void OnCompleted(Action continuation)またはUnsafeOnCompleted(Action continuation)メソッドを実装していること。
  • T GetResult()メソッド(またはvoid GetResult())が正しい結果を返すこと。

この仕様に従った実装が存在しない場合、コンパイラはCS1986エラーを発生させるため、非同期待機処理が正しく動作しなくなります。

static修飾子による問題

staticなGetAwaiterが引き起こすエラー

GetAwaiter()メソッドが静的staticで実装されている場合、コンパイラはインスタンスメソッドとして探すため、正しく候補が見つからなくなります。

たとえば、次のコードではMyTask<TResult>クラスのGetAwaiter()staticで宣言されているために、await式で使用する際にCS1986エラーが発生します。

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public class MyTask<TResult>
{
    readonly MyTaskAwaiter<TResult> awaiter;
    public MyTask(TResult value)
    {
        // 日本語の説明:awaiterの初期化
        this.awaiter = new MyTaskAwaiter<TResult>(value);
    }
    // エラーの原因:staticなGetAwaiterメソッドの宣言
    public static MyTaskAwaiter<TResult> GetAwaiter() => throw new NotImplementedException();
}
public class MyTaskAwaiter<TResult> : INotifyCompletion
{
    TResult value;
    public MyTaskAwaiter(TResult value)
    {
        // 日本語の説明:結果値をセット
        this.value = value;
    }
    public bool IsCompleted { get => true; }
    public TResult GetResult() => value;
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
}
class Program
{
    static async Task Main(string[] args)
    {
        // 日本語の説明:非同期処理の実行例
        MyTask<int> myTask = new MyTask<int>(100);
        int result = await myTask;
        Console.WriteLine(result);
    }
}
// 出力例:正常に処理が完了していれば、100が出力される想定

上記の例では、GetAwaiter()が静的であるため、インスタンスメソッドとしての要件を満たせず、コンパイラエラーとなります。

INotifyCompletionインターフェイスとの関係

await式で利用するオブジェクトのawaiterは、必ずINotifyCompletionインターフェイスを実装する必要があります。

このインターフェイスは、完了時に指定されたコールバックActionを呼び出すためのOnCompletedメソッドを持ちます。

正しい非同期動作には、以下のメンバーが必須です。

  • bool IsCompleted { get; }
  • void OnCompleted(Action continuation)
  • T GetResult()

これらのメンバーがそろっていることで、コンパイラは非同期処理の制御を行えるようになります。

もし、これらの条件が満たされない場合、await式は正しく動作せず、エラーが発生します。

コード例によるエラー解析

実際のコード例を通して、どのようにエラーが発生するかを解析します。

エラー発生コードの構造

MyTaskクラスにおけるGetAwaiter定義

問題の根本は、MyTask<TResult>クラス内のGetAwaiter()メソッドが静的に宣言されている点にあります。

クラスは以下のように定義されています。

using System;
using System.Runtime.CompilerServices;
public class MyTask<TResult>
{
    readonly MyTaskAwaiter<TResult> awaiter;
    public MyTask(TResult value)
    {
        // 日本語の説明:awaiterの初期化処理
        this.awaiter = new MyTaskAwaiter<TResult>(value);
    }
    // エラーの原因:static修飾子が付いているため、インスタンスメソッドとして認識されない
    public static MyTaskAwaiter<TResult> GetAwaiter() => throw new NotImplementedException();
}

この実装では、コンパイラはインスタンスのGetAwaiter()メソッドを探すため、staticで定義されているためエラーが発生します。

MyTaskAwaiterの実装と問題点

MyTaskAwaiter<TResult>クラスは、INotifyCompletionインターフェイスを実装し、待機状態や結果取得のための基本的なメソッドを持っています。

しかし、実装上の注意点は以下の通りです。

  • IsCompletedプロパティは正しく処理が完了しているかを返す役割を持っています。ここでは固定値trueを返しているため、即時完了とみなされます。
  • OnCompleted(Action continuation)メソッドは、実際には継続処理を設定する処理が必要ですが、例示ではNotImplementedExceptionが投げられています。
  • GetResult()メソッドは、実際の結果を返す機能を持っており、ここでは単純に初期化された値を返す実装になっています。

この実装自体には大きな問題はなく、主なエラー原因はMyTask<TResult>側のGetAwaiter()が静的に宣言されている点です。

エラー箇所の詳細検証

static指定の影響の確認

静的メソッドとしてのGetAwaiter()は、コンパイラにインスタンスのメソッドとして認識されません。

await式は、オブジェクトのインスタンスからGetAwaiter()を呼び出すことが前提であるため、静的メソッドは対象外となります。

このため、静的指定により本来のインスタンス待機処理が機能せず、CS1986エラーが発生します。

型の整合性チェック

さらに、GetAwaiter()が返す型は必ずINotifyCompletionインターフェイスを実装している必要があります。

型の整合性が取れていない場合、非同期処理の仕組みが正しく動作せず、エラーとなる可能性があります。

正しい実装では、返されたawaiter型が必ず以下の要件を満たすように定義する必要があります。

Awaiter型{bool IsCompleted { get; }void OnCompleted(Action continuation)T GetResult()

この点において、MyTaskAwaiter<TResult>は条件を満たしているため、型の整合性自体は問題ありません。

しかし、MyTask<TResult>側で正しくインスタンスメソッドとして定義されていないと、当然ながらエラーが発生します。

CS1986エラーの対策

エラーを解消するためには、主にGetAwaiter()の定義を正しくインスタンスメソッドとして実装する必要があります。

以下に、具体的な修正方法とサンプルコードを示します。

エラー解消のための修正手法

static修飾子の削除方法

まず、MyTask<TResult>クラス内のGetAwaiter()メソッドからstatic修飾子を削除します。

これにより、コンパイラはインスタンスメソッドとして認識できるようになります。

修正前後の違いは以下の通りです。

修正前

public static MyTaskAwaiter<TResult> GetAwaiter() => throw new NotImplementedException();

修正後

public MyTaskAwaiter<TResult> GetAwaiter() => awaiter;

上記のように、インスタンス変数awaiterを返す実装に変更することで、await式が正しく動作するようになります。

正しいGetAwaiter実装例の提示

以下は、修正後の正しい実装例です。

サンプルコードには、実行可能なMain関数も含めています。

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
// 非同期タスククラス
public class MyTask<TResult>
{
    readonly MyTaskAwaiter<TResult> awaiter;
    public MyTask(TResult value)
    {
        // 日本語:awaiterの初期化処理
        this.awaiter = new MyTaskAwaiter<TResult>(value);
    }
    // 修正済:static指定を削除し、インスタンスメソッドとして定義
    public MyTaskAwaiter<TResult> GetAwaiter() => awaiter;
}
// awaiterクラス
public class MyTaskAwaiter<TResult> : INotifyCompletion
{
    TResult value;
    public MyTaskAwaiter(TResult value)
    {
        // 日本語:値を設定
        this.value = value;
    }
    public bool IsCompleted { get => true; }
    public TResult GetResult() => value;
    public void OnCompleted(Action continuation)
    {
        // 日本語:継続処理の呼び出し(簡略化のため即時実行)
        continuation?.Invoke();
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        // 日本語:MyTaskを生成してawait処理を実行
        MyTask<int> myTask = new MyTask<int>(200);
        int result = await myTask;
        Console.WriteLine(result); // 結果として200が出力される
    }
}
200

上記のコードは、MyTask<TResult>クラスのGetAwaiter()メソッドが正しくインスタンスメソッドとして実装されているため、await myTaskが意図通りの動作を行い、結果が正常に出力されます。

修正後の動作確認

コンパイルチェックの実施

修正後のコードは、コンパイラが必要とするGetAwaiter()の署名を満たしていることを確認してください。

Visual StudioやCLIでコンパイルすることで、CS1986エラーが解消されていることが確認できます。

プロジェクト全体への影響確認

今回の修正は、MyTask<TResult>および関連するMyTaskAwaiter<TResult>に限定されるため、他の非同期処理とは独立して動作するはずです。

実際にプロジェクト全体でテストを実施し、他の部分との連携や動作に影響が出ないかを確認することが推奨されます。

まとめ

この記事では、C#の非同期処理におけるコンパイラエラーCS1986の原因と対策について解説しています。

awaitキーワードの動作原理や、返却するawaiterが持つべきメンバー、及びGetAwaiterがインスタンスメソッドである必要性を理解できます。

また、static指定が引き起こすエラーの原因を具体例で確認し、正しい実装方法(static修飾子の削除による修正)を習得できます。

関連記事

Back to top button