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型が必ず以下の要件を満たすように定義する必要があります。
この点において、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修飾子の削除による修正)を習得できます。