使用しているステートメント内で閉鎖内で可変構造変数をキャプチャするのは、なぜローカルな動作を変えるのですか?
-
09-10-2019 - |
質問
アップデート: :まあ、今私はそれを行ってやりました:私は Microsoftにバグレポートを提出しました これについて、私はそれが正しい行動であることを真剣に疑っているので。そうは言っても、私はまだ何について信じなければならないか100%確信がありません この質問;だから私は「正しい」ものが開かれていることがわかります いくつかの 解釈のレベル。
私の気持ちは、Microsoftがこれがバグであることを受け入れるか、または、可変値タイプ変数の変更が using
ステートメントは未定義の動作を構成します。
また、それが価値があるために、私は少なくとも 推測してみて ここで何が起こっているのか。コンパイラは、閉鎖のためのクラスを生成し、ローカル変数をそのクラスのインスタンスフィールドに「持ち上げる」ことを疑っています。そして、それは内にあるので using
ブロック、 それはフィールドを作っています readonly
. 。ルケが指摘したように 他の質問へのコメント, 、これにより、次のようなメソッド呼び出しが防止されます MoveNext
フィールド自体の変更から(代わりにコピーに影響します)。
注:読みやすさのためにこの質問を短縮しましたが、まだ正確ではありません。元の(長い)質問全体については、編集履歴を参照してください。
私はECMA-334の関連セクションであると私が信じていることを読みました、そして、この質問に対する決定的な答えを見つけることができないようです。最初に質問を述べ、次に興味のある人への追加のコメントへのリンクを提供します。
質問
実装する可変値タイプがある場合 IDisposable
, 、(1)ローカル変数の値の状態を変更する方法を呼び出すことができます using
声明とコードは私が期待するように動作します。閉鎖内で問題の変数をキャプチャしたら 以内に using
ただし、(2)値の変更は、ローカルスコープではもはや表示されません。
この動作は、変数が閉鎖内でキャプチャされる場合にのみ明らかです と 間に using
声明; 1つだけの場合は明らかではありません(using
)または他の条件(閉鎖)が存在します。
なぜ閉鎖内の変動値タイプの変数をキャプチャするのか using
ステートメントはローカルな動作を変更しますか?
以下は項目1と2を示すコード例です。両方の例は、次のデモを利用します Mutable
値タイプ:
struct Mutable : IDisposable
{
int _value;
public int Increment()
{
return _value++;
}
public void Dispose() { }
}
1. a内の値タイプ変数を変異させる using
ブロック
using (var x = new Mutable())
{
Console.WriteLine(x.Increment());
Console.WriteLine(x.Increment());
}
出力コード出力:
0 1
2.内部の閉鎖内で値タイプ変数をキャプチャする using
ブロック
using (var x = new Mutable())
{
// x is captured inside a closure.
Func<int> closure = () => x.Increment();
// Now the Increment method does not appear to affect the value
// of local variable x.
Console.WriteLine(x.Increment());
Console.WriteLine(x.Increment());
}
上記のコード出力:
0 0
さらなるコメント
モノコンパイラが私が期待する動作を提供することに注意されています(ローカル変数の値の変更はまだ表示されます using
+閉鎖ケース)。この行動が正しいかどうかは私にとって不明です。
この問題についての私の考えについては、 ここ.
解決
それは既知のバグです。数年前に発見しました。修正は潜在的に破壊され、問題はかなりあいまいです。これらはそれを修正することに対するポイントです。したがって、実際に修正するのに十分高く優先されたことはありません。
これは、数年前から潜在的なブログのトピックの列に並んでいます。おそらく私はそれを書くべきです。
ちなみに、バグを説明するメカニズムに関する推測は完全に正確です。そこに素敵なサイキックデバッグ。
だから、はい、既知のバグですが、それに関係なくレポートをありがとう!
他のヒント
これは、閉鎖タイプの生成と使用方法に関係しています。 CSCがこれらのタイプを使用する方法には、微妙なバグがあるようです。たとえば、Movenext()を呼び出すときにMonoのGMCによって生成されたILは次のとおりです。
IL_0051: ldloc.3
IL_0052: ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Foo/'<Main>c__AnonStorey0'::enumerator
IL_0057: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
フィールドのアドレスをロードしていることに注意してください。これにより、メソッド呼び出しが閉鎖オブジェクトに保存されている値タイプのインスタンスを変更できるようにします。これは私が正しい動作であると考えるものであり、これにより、リストのコンテンツが正常に列挙されます。
CSCが生成するものは次のとおりです。
IL_0068: ldloc.3
IL_0069: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
IL_006e: stloc.s 5
IL_0070: ldloca.s 5
IL_0072: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
したがって、この場合、値タイプインスタンスのコピーを取得し、コピーのメソッドを呼び出しています。これがあなたをどこにも行かないのかは驚くべきことではありません。 get_current()呼び出しも同様に間違っています:
IL_0052: ldloc.3
IL_0053: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
IL_0058: stloc.s 5
IL_005a: ldloca.s 5
IL_005c: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0061: call void class [mscorlib]System.Console::WriteLine(int32)
列挙者の状態はコピーしているので、get_current()は明らかにreturnsと呼ばれていません。 default(int)
.
要するに、CSCはバギーのようです。 Ms.Netがそうではなかった間、Monoがこれを正しくしたことは興味深いです!
...この特定の奇妙さについてのジョン・スキートのコメントを聞いてみたいです。
#MonoのBrajkovicとの議論で、彼はC#言語の仕様に実際には詳述されていないと判断しました どうやって 閉鎖タイプを実装する必要も、閉鎖時にキャプチャされた地元の人々のアクセスが翻訳されるべきでもありません。スペックの実装の例は、CSCが使用する「コピー」メソッドを使用しているようです。したがって、どちらのコンパイラ出力も言語仕様に従って正しいと見なすことができますが、CSCはメソッド呼び出し後に少なくともローカルをクロージャーオブジェクトにコピーする必要があると主張します。
編集 - これは間違っています、私は質問を十分に注意深く読みませんでした。
構造体を閉鎖に配置すると、割り当てが生じます。バリュータイプの割り当てにより、タイプのコピーが得られます。だから何が起こっているのかあなたは新しいものを作成しているということです Enumerator<int>
, 、 と Current
その列挙では0が返されます。
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> l = new List<int>();
Console.WriteLine(l.GetEnumerator().Current);
}
}
結果:0
問題は、列挙者が別のクラスに保存されているため、すべてのアクションが列挙器のコピーで動作していることです。
[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
// Fields
public List<int>.Enumerator enumerator;
// Methods
public int <Main>b__1()
{
return this.enumerator.Current;
}
}
public static void Main(string[] args)
{
List<int> <>g__initLocal0 = new List<int>();
<>g__initLocal0.Add(1);
<>g__initLocal0.Add(2);
<>g__initLocal0.Add(3);
List<int> list = <>g__initLocal0;
Func<int> CS$<>9__CachedAnonymousMethodDelegate2 = null;
<>c__DisplayClass3 CS$<>8__locals4 = new <>c__DisplayClass3();
CS$<>8__locals4.enumerator = list.GetEnumerator();
try
{
if (CS$<>9__CachedAnonymousMethodDelegate2 == null)
{
CS$<>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS$<>8__locals4.<Main>b__1);
}
while (CS$<>8__locals4.enumerator.MoveNext())
{
Console.WriteLine(CS$<>8__locals4.enumerator.Current);
}
}
finally
{
CS$<>8__locals4.enumerator.Dispose();
}
}
ラムダがなければ、コードはあなたが期待するものに近いです。
public static void Main(string[] args)
{
List<int> <>g__initLocal0 = new List<int>();
<>g__initLocal0.Add(1);
<>g__initLocal0.Add(2);
<>g__initLocal0.Add(3);
List<int> list = <>g__initLocal0;
using (List<int>.Enumerator enumerator = list.GetEnumerator())
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
特定のIL
L_0058: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Machete.Runtime.Environment/<>c__DisplayClass3::enumerator
L_005d: stloc.s CS$0$0001
L_005f: ldloca.s CS$0$0001