提交 9a2b51f2 编写于 作者: J Julien Couvreur

tweaks

上级 e9576c07
......@@ -101,8 +101,9 @@ The promise of a value-or-end is returned from `MoveNextAsync`. It can be fulfil
The promise is implemented as a `ManualResetValueTaskSourceLogic<bool>` (which is a re-usable and allocation-free way of producing and fulfilling `ValueTask<bool>` instances) and its surrounding interfaces on the state machine: `IValueTaskSource<bool>` and `IStrongBox<ManualResetValueTaskSourceLogic<bool>>`.
Compared to the state machine for a regular async method, the `MoveNext()` for an async-iterator method adds logic:
- to support handling a `yield return` statement, which saves the current value and fulfill the promise with result `true`,
- to support handling a `yield return` statement, which saves the current value and fulfills the promise with result `true`,
- to support handling a `yield break` statement, which fulfills the promise with result `false`,
- to exit the method, which fulfills the promise with result `false`,
- to the handling of exceptions, to set the exception into the promise.
(The handling of an `await` is unchanged)
......@@ -110,14 +111,12 @@ This is reflected in the implementation, which extends the lowering machinery fo
1. handle `yield return` and `yield break` statements (add methods `VisitYieldReturnStatement` and `VisitYieldBreakStatement` to `AsyncMethodToStateMachineRewriter`),
2. produce additional state and logic for the promise itself (we use `AsyncIteratorRewriter` instead of `AsyncRewriter` to drive the lowering, and produces the other members: `MoveNextAsync`, `Current`, `DisposeAsync`, and some members supporting the resettable `ValueTask`, namely `GetResult`, `SetStatus`, `OnCompleted` and `Value.get`).
**open issue**: The compiler leverages existing BCL types (including some recently added types from the `System.Threading.Tasks.Extensions` NuGet package) in the state machine it generates. But as part of this feature, we may introduce some additional BCL types, so that the state machine can be further simplified and optimized.
```C#
ValueTask<bool> MoveNextAsync()
{
if (State == StateMachineStates.FinishedStateMachine)
{
return default(ValueTask<bool>)
return default(ValueTask<bool>);
}
_valueOrEndPromise.Reset();
var inst = this;
......@@ -129,40 +128,3 @@ ValueTask<bool> MoveNextAsync()
```C#
T Current => _current;
```
In terms of the lowered code, there are five changes to the `MoveNext()` method of an async-iterator state machine, compared to that of a regular `async` state machines:
- reaching the end of the method
- reaching an `await`
- reaching a `yield return`
- reaching a `yield break`
- handling exceptions
Those changes are described below for information, but they are compiler implementation details which should not be depended on (such generated code is subject to change without notice).
When we reach the end of the method, we need to fulfill the promise of value-or-end with `false` to signal that the end was reached:
```C#
_promiseOfValueOrEnd.SetResult(false);
```
When we reach a `yield return`, we save the "current" value and fulfill the promise of value-or-end with `true` to signal that a value is available:
```C#
_valueOrEndPromise.SetResult(true);
```
When we reach a `yield break`, we fulfill the promise of value-or-end with `false` to signal that the end was reached:
```C#
_promiseOfValueOrEnd.SetResult(false);
return;
```
The `MoveNext()` method of the state machine also includes exception handling.
For regular `async` methods, we catch any such exception and pass it on to the caller of the state machine, by setting the exception in the task being awaited by the caller.
For async-iterators, we also catch any such exception and pass it on to the caller of the state machine (`MoveNextAsync`) via the promise:
```C#
catch (Exception ex)
{
_state = finishedState;
_promiseOfValueOrEnd.SetException(ex);
return;
}
```
......@@ -198,7 +198,7 @@ private void CheckRequiredLangVersionForAsyncIteratorMethods(DiagnosticBag diagn
var symbol = this.ContainingMemberOrLambda as MethodSymbol;
if (symbol.IsAsync)
{
MessageID.IDS_FeatureAsyncStreams.CheckFeatureAvailability(availableVersion: Compilation.LanguageVersion, diagnostics: diagnostics, errorLocation: symbol.Locations[0]);
MessageID.IDS_FeatureAsyncStreams.CheckFeatureAvailability(availableVersion: Compilation.LanguageVersion, diagnostics, symbol.Locations[0]);
}
}
......
......@@ -495,8 +495,10 @@ private BoundForEachStatement BindForEachPartsWorker(DiagnosticBag diagnostics,
private bool GetAwaitDisposeAsyncInfo(ref ForEachEnumeratorInfo.Builder builder, DiagnosticBag diagnostics)
{
BoundExpression placeholder = new BoundAwaitableValuePlaceholder(_syntax.Expression,
this.GetWellKnownType(WellKnownType.System_Threading_Tasks_ValueTask, diagnostics, this._syntax));
bool hasErrors = false;
BoundExpression placeholder = new BoundAwaitableValuePlaceholder(_syntax.Expression, this.GetWellKnownType(WellKnownType.System_Threading_Tasks_ValueTask, diagnostics, this._syntax));
builder.DisposeAwaitableInfo = BindAwaitInfo(placeholder, _syntax.Expression, _syntax.AwaitKeyword.GetLocation(), diagnostics, ref hasErrors);
return hasErrors;
}
......@@ -588,9 +590,7 @@ private void UnwrapCollectionExpressionIfNullable(ref BoundExpression collection
/// 1) Collection type
/// 2) Element type
/// 3) GetEnumerator (or GetAsyncEnumerator) method of the collection type (return type will be the enumerator type from the spec)
/// 4) Either:
/// - Current property and MoveNext method of the enumerator type
/// - WaitForNextAsync method and TryGetNext of the enumerator type (for async-foreach)
/// 4) Current property and MoveNext (or MoveNextAsync) method of the enumerator type
///
/// The caller will have to do some extra conversion checks before creating a ForEachEnumeratorInfo for the BoundForEachStatement.
/// </summary>
......@@ -791,7 +791,8 @@ private EnumeratorResult GetEnumeratorInfo(ref ForEachEnumeratorInfo.Builder bui
builder.CurrentPropertyGetter = currentPropertyGetter.AsMember((NamedTypeSymbol)enumeratorType);
}
}
else if (!isAsync)
if (!isAsync)
{
// NOTE: MoveNext is actually inherited from System.Collections.IEnumerator
builder.MoveNextMethod = (MethodSymbol)GetSpecialTypeMember(SpecialMember.System_Collections_IEnumerator__MoveNext, diagnostics, errorLocationSyntax);
......@@ -898,7 +899,7 @@ private bool SatisfiesGetEnumeratorPattern(ref ForEachEnumeratorInfo.Builder bui
{
LookupResult lookupResult = LookupResult.GetInstance();
string methodName = isAsync ? GetAsyncEnumeratorMethodName : GetEnumeratorMethodName;
MethodSymbol getEnumeratorMethod = FindForEachPatternMethod(collectionExprType, methodName, lookupResult, warningsOnly: true, diagnostics, isAsync);
MethodSymbol getEnumeratorMethod = FindForEachPatternMethod(collectionExprType, methodName, lookupResult, warningsOnly: true, diagnostics: diagnostics, isAsync: isAsync);
lookupResult.Free();
builder.GetEnumeratorMethod = getEnumeratorMethod;
......@@ -914,9 +915,8 @@ private bool SatisfiesGetEnumeratorPattern(ref ForEachEnumeratorInfo.Builder bui
/// <param name="lookupResult">Passed in for reusability.</param>
/// <param name="warningsOnly">True if failures should result in warnings; false if they should result in errors.</param>
/// <param name="diagnostics">Populated with binding diagnostics.</param>
/// <param name="arguments">If unspecified, we'll look for a method with no arguments. You can specify arguments, for example when looking for the `TryGetNext(out bool)` method.</param>
/// <returns>The desired method or null.</returns>
private MethodSymbol FindForEachPatternMethod(TypeSymbol patternType, string methodName, LookupResult lookupResult, bool warningsOnly, DiagnosticBag diagnostics, bool isAsync, AnalyzedArguments arguments = null)
private MethodSymbol FindForEachPatternMethod(TypeSymbol patternType, string methodName, LookupResult lookupResult, bool warningsOnly, DiagnosticBag diagnostics, bool isAsync)
{
Debug.Assert(lookupResult.IsClear);
......@@ -963,13 +963,13 @@ private MethodSymbol FindForEachPatternMethod(TypeSymbol patternType, string met
// some custom logic in ExpressionBinder.BindGrpToParams. The biggest difference
// we've found (so far) is that it only considers methods with expected number of parameters
// (i.e. doesn't work with "params" or optional parameters).
if (method.ParameterCount == (arguments?.Arguments.Count ?? 0))
if (method.ParameterCount == 0)
{
candidateMethods.Add((MethodSymbol)member);
}
}
MethodSymbol patternMethod = PerformForEachPatternOverloadResolution(patternType, candidateMethods, warningsOnly, diagnostics, arguments, isAsync);
MethodSymbol patternMethod = PerformForEachPatternOverloadResolution(patternType, candidateMethods, warningsOnly, diagnostics, isAsync);
candidateMethods.Free();
......@@ -980,14 +980,10 @@ private MethodSymbol FindForEachPatternMethod(TypeSymbol patternType, string met
/// The overload resolution portion of FindForEachPatternMethod.
/// If no arguments are passed in, then an empty argument list will be used.
/// </summary>
private MethodSymbol PerformForEachPatternOverloadResolution(TypeSymbol patternType, ArrayBuilder<MethodSymbol> candidateMethods, bool warningsOnly, DiagnosticBag diagnostics, AnalyzedArguments arguments, bool isAsync)
private MethodSymbol PerformForEachPatternOverloadResolution(TypeSymbol patternType, ArrayBuilder<MethodSymbol> candidateMethods, bool warningsOnly, DiagnosticBag diagnostics, bool isAsync)
{
Debug.Assert(arguments is null || arguments.Arguments.Count > 0);
ArrayBuilder<TypeSymbol> typeArguments = ArrayBuilder<TypeSymbol>.GetInstance();
if (arguments == null)
{
arguments = AnalyzedArguments.GetInstance();
}
AnalyzedArguments arguments = AnalyzedArguments.GetInstance();
OverloadResolutionResult<MethodSymbol> overloadResolutionResult = OverloadResolutionResult<MethodSymbol>.GetInstance();
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
......@@ -1047,7 +1043,7 @@ private MethodSymbol PerformForEachPatternOverloadResolution(TypeSymbol patternT
/// Called after it is determined that the expression being enumerated is of a type that
/// has a GetEnumerator (or GetAsyncEnumerator) method. Checks to see if the return type of the GetEnumerator
/// method is suitable (i.e. has Current and MoveNext for regular case,
/// or Current and WaitForNextAsync for async case).
/// or Current and MoveNextAsync for async case).
/// </summary>
/// <param name="builder">Must be non-null and contain a non-null GetEnumeratorMethod.</param>
/// <param name="diagnostics">Will be populated with pattern diagnostics.</param>
......@@ -1142,7 +1138,8 @@ private bool SatisfiesForEachPattern(ref ForEachEnumeratorInfo.Builder builder,
lookupResult.Clear(); // Reuse the same LookupResult
MethodSymbol moveNextMethodCandidate = FindForEachPatternMethod(enumeratorType, isAsync ? MoveNextAsyncMethodName : MoveNextMethodName, lookupResult, warningsOnly: false, diagnostics, isAsync);
MethodSymbol moveNextMethodCandidate = FindForEachPatternMethod(enumeratorType,
isAsync ? MoveNextAsyncMethodName : MoveNextMethodName, lookupResult, warningsOnly: false, diagnostics: diagnostics, isAsync: isAsync);
if ((object)moveNextMethodCandidate == null ||
moveNextMethodCandidate.IsStatic || moveNextMethodCandidate.DeclaredAccessibility != Accessibility.Public ||
......
......@@ -95,13 +95,6 @@
<Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/>
</Node>
<!--
This node is used to represent the argument for a TryGetNext(out bool) method.
It is used to perform initial binding only.
-->
<Node Name="BoundTryGetNextArgumentPlaceholder" Base="BoundValuePlaceholderBase">
</Node>
<!--
This node is used to represent an awaitable expression of a certain type, when binding an using-await statement.
It does not survive past initial binding.
......
......@@ -26,7 +26,6 @@ internal enum BoundKind: byte
GlobalStatementInitializer,
DeconstructValuePlaceholder,
TupleOperandPlaceholder,
TryGetNextArgumentPlaceholder,
AwaitableValuePlaceholder,
Dup,
PassByCopy,
......@@ -465,42 +464,6 @@ public BoundTupleOperandPlaceholder Update(TypeSymbol type)
}
}
internal sealed partial class BoundTryGetNextArgumentPlaceholder : BoundValuePlaceholderBase
{
public BoundTryGetNextArgumentPlaceholder(SyntaxNode syntax, TypeSymbol type, bool hasErrors)
: base(BoundKind.TryGetNextArgumentPlaceholder, syntax, type, hasErrors)
{
Debug.Assert(type != null, "Field 'type' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)");
}
public BoundTryGetNextArgumentPlaceholder(SyntaxNode syntax, TypeSymbol type)
: base(BoundKind.TryGetNextArgumentPlaceholder, syntax, type)
{
Debug.Assert(type != null, "Field 'type' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)");
}
public override BoundNode Accept(BoundTreeVisitor visitor)
{
return visitor.VisitTryGetNextArgumentPlaceholder(this);
}
public BoundTryGetNextArgumentPlaceholder Update(TypeSymbol type)
{
if (type != this.Type)
{
var result = new BoundTryGetNextArgumentPlaceholder(this.Syntax, type, this.HasErrors);
result.WasCompilerGenerated = this.WasCompilerGenerated;
return result;
}
return this;
}
}
internal sealed partial class BoundAwaitableValuePlaceholder : BoundValuePlaceholderBase
{
public BoundAwaitableValuePlaceholder(SyntaxNode syntax, TypeSymbol type, bool hasErrors)
......@@ -6493,8 +6456,6 @@ internal R VisitInternal(BoundNode node, A arg)
return VisitDeconstructValuePlaceholder(node as BoundDeconstructValuePlaceholder, arg);
case BoundKind.TupleOperandPlaceholder:
return VisitTupleOperandPlaceholder(node as BoundTupleOperandPlaceholder, arg);
case BoundKind.TryGetNextArgumentPlaceholder:
return VisitTryGetNextArgumentPlaceholder(node as BoundTryGetNextArgumentPlaceholder, arg);
case BoundKind.AwaitableValuePlaceholder:
return VisitAwaitableValuePlaceholder(node as BoundAwaitableValuePlaceholder, arg);
case BoundKind.Dup:
......@@ -6827,10 +6788,6 @@ public virtual R VisitTupleOperandPlaceholder(BoundTupleOperandPlaceholder node,
{
return this.DefaultVisit(node, arg);
}
public virtual R VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node, A arg)
{
return this.DefaultVisit(node, arg);
}
public virtual R VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node, A arg)
{
return this.DefaultVisit(node, arg);
......@@ -7459,10 +7416,6 @@ public virtual BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlacehold
{
return this.DefaultVisit(node);
}
public virtual BoundNode VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node)
{
return this.DefaultVisit(node);
}
public virtual BoundNode VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node)
{
return this.DefaultVisit(node);
......@@ -8095,10 +8048,6 @@ public override BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlacehol
{
return null;
}
public override BoundNode VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node)
{
return null;
}
public override BoundNode VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node)
{
return null;
......@@ -8917,11 +8866,6 @@ public override BoundNode VisitTupleOperandPlaceholder(BoundTupleOperandPlacehol
TypeSymbol type = this.VisitType(node.Type);
return node.Update(type);
}
public override BoundNode VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node)
{
TypeSymbol type = this.VisitType(node.Type);
return node.Update(type);
}
public override BoundNode VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node)
{
TypeSymbol type = this.VisitType(node.Type);
......@@ -9886,14 +9830,6 @@ public override TreeDumperNode VisitTupleOperandPlaceholder(BoundTupleOperandPla
}
);
}
public override TreeDumperNode VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node, object arg)
{
return new TreeDumperNode("tryGetNextArgumentPlaceholder", null, new TreeDumperNode[]
{
new TreeDumperNode("type", node.Type, null)
}
);
}
public override TreeDumperNode VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node, object arg)
{
return new TreeDumperNode("awaitableValuePlaceholder", null, new TreeDumperNode[]
......
......@@ -21,8 +21,8 @@ internal sealed class AsyncIteratorInfo
// Method to fulfill the promise with an exception: `void ManualResetValueTaskSourceLogic<T>.SetException(Exception error)`
internal MethodSymbol SetExceptionMethod { get; }
public AsyncIteratorInfo(FieldSymbol promiseOfValueOrEndField, FieldSymbol currentField, MethodSymbol setResultMethod,
MethodSymbol setExceptionMethod)
public AsyncIteratorInfo(FieldSymbol promiseOfValueOrEndField, FieldSymbol currentField,
MethodSymbol setResultMethod, MethodSymbol setExceptionMethod)
{
PromiseOfValueOrEndField = promiseOfValueOrEndField;
CurrentField = currentField;
......
......@@ -150,36 +150,36 @@ protected override BoundStatement GenerateStateMachineCreation(LocalSymbol state
}
/// <summary>
/// Generates the WaitForNextAsync method.
/// Generates the `ValueTask&lt;bool> MoveNextAsync()` method.
/// </summary>
private void GenerateIAsyncEnumeratorImplementation_MoveNextAsync()
{
// Produce the implementation for `ValueTask<bool> MoveNextAsync()`:
// if (State == StateMachineStates.FinishedStateMachine)
// {
// return default(ValueTask<bool>)
// }
// _valueOrEndPromise.Reset();
// var inst = this;
// _builder.Start(ref inst);
// return new ValueTask<bool>(this, _valueOrEndPromise.Version);
// Produce:
// if (State == StateMachineStates.FinishedStateMachine)
// {
// return default(ValueTask<bool>)
// }
// _valueOrEndPromise.Reset();
// var inst = this;
// _builder.Start(ref inst);
// return new ValueTask<bool>(this, _valueOrEndPromise.Version);
NamedTypeSymbol IAsyncEnumeratorOfElementType =
F.WellKnownType(WellKnownType.System_Collections_Generic_IAsyncEnumerator_T)
.Construct(_currentField.Type);
MethodSymbol IAsyncEnumerableOfElementType_WaitForNextAsync =
MethodSymbol IAsyncEnumerableOfElementType_MoveNextAsync =
F.WellKnownMethod(WellKnownMember.System_Collections_Generic_IAsyncEnumerator_T__MoveNextAsync)
.AsMember(IAsyncEnumeratorOfElementType);
// The implementation doesn't depend on the method body of the iterator method.
OpenMethodImplementation(IAsyncEnumerableOfElementType_WaitForNextAsync, hasMethodBodyDependency: false);
OpenMethodImplementation(IAsyncEnumerableOfElementType_MoveNextAsync, hasMethodBodyDependency: false);
var ifFinished = F.If(
// if (State == StateMachineStates.FinishedStateMachine)
F.IntEqual(F.Field(F.This(), stateField), F.Literal(StateMachineStates.FinishedStateMachine)),
// return default(ValueTask<bool>)
thenClause: F.Return(F.Default(IAsyncEnumerableOfElementType_WaitForNextAsync.ReturnType)));
thenClause: F.Return(F.Default(IAsyncEnumerableOfElementType_MoveNextAsync.ReturnType)));
// _promiseOfValueOrEnd.Reset();
BoundFieldAccess promiseField = F.Field(F.This(), _promiseOfValueOrEndField);
......@@ -201,7 +201,7 @@ private void GenerateIAsyncEnumeratorImplementation_MoveNextAsync()
MethodSymbol valueTask_ctor =
F.WellKnownMethod(WellKnownMember.System_Threading_Tasks_ValueTask_T__ctor)
.AsMember((NamedTypeSymbol)IAsyncEnumerableOfElementType_WaitForNextAsync.ReturnType);
.AsMember((NamedTypeSymbol)IAsyncEnumerableOfElementType_MoveNextAsync.ReturnType);
MethodSymbol promise_get_Version =
F.WellKnownMethod(WellKnownMember.System_Threading_Tasks_ManualResetValueTaskSourceLogic_T__get_Version)
......
......@@ -770,12 +770,6 @@ public override BoundNode VisitDeconstructValuePlaceholder(BoundDeconstructValue
return null;
}
public override BoundNode VisitTryGetNextArgumentPlaceholder(BoundTryGetNextArgumentPlaceholder node)
{
Fail(node);
return null;
}
private void Fail(BoundNode node)
{
Debug.Assert(false, $"Bound nodes of kind {node.Kind} should not survive past local rewriting");
......
......@@ -682,13 +682,14 @@ static async System.Threading.Tasks.Task Main()
Write($""{value} "");
found = await enumerator.MoveNextAsync();
if (found) throw null;
found = await enumerator.MoveNextAsync();
if (found) throw null;
Write(""5"");
}
}
}";
var comp = CreateCompilationWithTasksExtensions(new[] { source, s_common }, options: TestOptions.DebugExe);
comp.VerifyDiagnostics();
var c = CompileAndVerify(comp);
CompileAndVerify(comp, expectedOutput: "0 1 2 3 4 5");
}
......
......@@ -29,20 +29,6 @@ class E2
public object Current { get; }
public void Dispose() { }
}
class E3
{
public E GetAsyncEnumerator() => throw null;
public ValueTask<bool> WaitForNextAsync() => throw null;
public object TryGetNext(out bool success) => throw null;
public ValueTask DisposeAsync() => throw null;
}
class E4
{
public E GetAsyncEnumerator() => throw null;
public ValueTask<bool> WaitForNextAsync() => throw null;
public object TryGetNext(out bool success) => throw null;
public ValueTask DisposeAsync() => throw null;
}
");
var e1 = (TypeSymbol)c.GlobalNamespace.GetMembers("E1").Single();
var ge1 = (MethodSymbol)e1.GetMembers("GetEnumerator").Single();
......@@ -66,30 +52,6 @@ class E4
EqualityTesting.AssertNotEqual(new ForEachStatementInfo(ge1, mn1, cur1, disp2, e1, conv1, conv1), new ForEachStatementInfo(ge1, mn1, cur1, disp1, e1, conv1, conv1));
EqualityTesting.AssertNotEqual(new ForEachStatementInfo(ge1, mn1, cur1, disp1, e1, conv2, conv1), new ForEachStatementInfo(ge1, mn1, cur1, disp1, e1, conv1, conv1));
EqualityTesting.AssertNotEqual(new ForEachStatementInfo(ge1, mn1, cur1, disp1, e1, conv1, conv2), new ForEachStatementInfo(ge1, mn1, cur1, disp1, e1, conv1, conv1));
var e3 = (TypeSymbol)c.GlobalNamespace.GetMembers("E3").Single();
var gae3 = (MethodSymbol)e3.GetMembers("GetAsyncEnumerator").Single();
var wfna3 = (MethodSymbol)e3.GetMembers("WaitForNextAsync").Single();
var tgn3 = (MethodSymbol)e3.GetMembers("TryGetNext").Single();
var disp3 = (MethodSymbol)e3.GetMembers("DisposeAsync").Single();
var conv3 = Conversion.NoConversion;
var e4 = (TypeSymbol)c.GlobalNamespace.GetMembers("E4").Single();
var gae4 = (MethodSymbol)e4.GetMembers("GetAsyncEnumerator").Single();
var wfna4 = (MethodSymbol)e4.GetMembers("WaitForNextAsync").Single();
var tgn4 = (MethodSymbol)e4.GetMembers("TryGetNext").Single();
var disp4 = (MethodSymbol)e4.GetMembers("DisposeAsync").Single();
var conv4 = Conversion.NoConversion;
EqualityTesting.AssertEqual(
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna3, tgn3),
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna3, tgn3));
EqualityTesting.AssertNotEqual(
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna3, tgn3),
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna4, tgn3));
EqualityTesting.AssertNotEqual(
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna3, tgn3),
new ForEachStatementInfo(gae3, moveNextMethod: null, currentProperty: null, disp3, e3, conv3, conv3, wfna3, tgn4));
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册