LoginSignup
14
10

More than 5 years have passed since last update.

yieldの罠

Last updated at Posted at 2013-12-17

伝えたかった事とずれまくっていたので書き直しです。
今回は yield を使用したソースがターゲットです。

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main( string[ ] args )
        {
            try { Yield( ); Console.WriteLine( "Yield: 通過" ); }
            catch { Console.WriteLine( "Yield: エラー" ); }
            try { NonYield( ); Console.WriteLine( "NonYield: 通過" ); }
            catch { Console.WriteLine( "NonYield: エラー" ); }
        }
        static IEnumerable<int> Yield( )
        {
            throw new InvalidOperationException( );
            yield return 0;
        }
        static IEnumerable<int> NonYield( )
        {
            throw new InvalidOperationException( );
            return new[ ] { 0 };
        }
    }
}

結果は次の通りです。

Yield: 通過
NonYield: エラー

('ω')

結果を見て分かるように、Yield メソッドでは例外が発生しませんでした。
前回同様に逆コンパイルしてコードを見てみます。

[CompilerGenerated]
private sealed class <Yield>d__0
    : IEnumerable<int>, IEnumerable, 
      IEnumerator<int>, IEnumerator, IDisposable
{
    // 省略
}
private static void Main( string[ ] args )
{
    // 省略
} 
private static IEnumerable<int> Yield( )
{
    return new Program.<Yield>d__0( -2 );
}
private static IEnumerable<int> NonYield( )
{
    throw new InvalidOperationException( );
}

('ω')  (d__0)

( 'ω')

ビルドは Debug ですが、 return まで到達しないので NonYieldthrow 文のみに
なっています。Yield メソッドは代わりにインスタンスを返却するコードに変換されています。
このクラスはコンパイラが作成したもので、実装(MoveNextメソッド)に元々記述していた
処理(throw 文)が含まれています。

コンパイラは yield を指定した時点で 対象メンバの処理 をすべて移すようで、
今回記述した throw 文もこの中に移動されていました。

// d__0 クラス内
bool IEnumerator.MoveNext( )
{
    int num = this.<>1__state;
    if ( num != 0 )
    {
        return false;
    }
    this.<>1__state = -1;
    throw new InvalidOperationException( );
}

ようするに IEnumerable なメンバの処理に yield を使用すると、コンパイラ製クラスの
インスタンス作成&返却 という処理に置き換えられるため、throw 文は反復処理が
開始されるまで実行されないということでした。

元々 return で書いていたメンバの処理を yield return に置き換えたり、
yield return で書いていたメンバの処理を LINQ に置き換えたりすることは、今回のように
評価タイミングにズレが生じる結果になるので、周辺コードに注意することをお勧めします。

条件が Code Contracts で記述された場合は結果は異なります。
以下ソースコードと実行結果&逆コンパイルで終わり。

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main( string[ ] args )
        {
            try { Yield( ); Console.WriteLine( "Yield: 通過" ); }
            catch { Console.WriteLine( "Yield: エラー" ); }
            try { NonYield( ); Console.WriteLine( "NonYield: 通過" ); }
            catch { Console.WriteLine( "NonYield: エラー" ); }
            Console.ReadKey( );
        }
        static IEnumerable<int> Yield( )
        {
            Contract.Requires<InvalidOperationException>( false );
            yield return 0;
        }
        static IEnumerable<int> NonYield( )
        {
            Contract.Requires<InvalidOperationException>( false );
            return new[ ] { 0 };
        }
    }
}

実行結果

Yield: エラー
NonYield: エラー

逆コンパイル

private static IEnumerable<int> Yield( )
{
    __ContractsRuntime.Requires<InvalidOperationException>( false, null, "false" );
    return new Program.<Yield>d__0( -2 );
}
private static IEnumerable<int> NonYield( )
{
    __ContractsRuntime.Requires<InvalidOperationException>( false, null, "false" );
    return new int[ 1 ];
}
14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10