public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Action triggerAction) { var expectedSequence = new Queue<int>(); for (int i = 0; i < subscribeActions.Count; i++) { expectedSequence.Enqueue(i); } ExpectEventSequence(subscribeActions,triggerAction,expectedSequence); } public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Action triggerAction,Queue<int> expectedSequence) { var fired = new Queue<int>(); var actionsCount = subscribeActions.Count; for(var i =0; i< actionsCount;i++) { subscription((o,e) => { fired.Enqueue(i); }); } triggerAction(); var executionIndex = 0; var inOrder = true; foreach (var firedIndex in fired) { if (firedIndex != expectedSequence.Dequeue()) { inOrder = false; break; } executionIndex++; } if (subscribeActions.Count != fired.Count) { Assert.Fail("Not all events were fired."); } if (!inOrder) { Assert.Fail(string.Format( CultureInfo.CurrentCulture,"Events were not fired in the expected sequence from element {0}",executionIndex)); } }
示例用法如下:
[Test()] public void FillFuel_Test([Values(1,5,10,100)]float maxFuel) { var fuelTank = new FuelTank() { MaxFuel = maxFuel }; var eventHandlerSequence = new Queue<Action<EventHandler>>(); eventHandlerSequence.Enqueue(x => fuelTank.FuelFull += x); //Dealing with a subclass of EventHandler eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged += (o,e) => x(o,e)); Test.ExpectEventSequence(eventHandlerSequence,() => fuelTank.FillFuel()); }
并且测试的代码:
public float Fuel { get { return fuel; } private set { var adjustedFuel = Math.Max(0,Math.Min(value,MaxFuel)); if (fuel != adjustedFuel) { var oldFuel = fuel; fuel = adjustedFuel; RaiseCheckFuelChangedEvents(oldFuel); } } } public void FillFuel() { Fuel = MaxFuel; } private void RaiseCheckFuelChangedEvents(float oldFuel) { FuelChanged.FireEvent(this,new FuelEventArgs(oldFuel,Fuel)); if (fuel == 0) { FuelEmpty.FireEvent(this,EventArgs.Empty); } else if (fuel == MaxFuel) { FuelFull.FireEvent(this,EventArgs.Empty); } if (oldFuel == 0 && Fuel != 0) { FuelNoLongerEmpty.FireEvent(this,EventArgs.Empty); } else if (oldFuel == MaxFuel && Fuel != MaxFuel) { FuelNoLongerFull.FireEvent(this,EventArgs.Empty); } }
因此,测试预计FuelFilled将在FuelChanged之前被解雇,但实际上FuelChanged首先被解雇,但未通过测试.
然而,我的测试是报告FuelChanged被触发两次,但是当我单步执行代码时,很明显FuelFhanged和FuelChanged只触发一次后燃烧FuelFilled.
我认为这与lambdas使用本地状态的方式有关,也许for循环迭代器变量只被设置为最终值,所以我用这个取代了for循环:
var subscriptions = subscribeActions.ToList(); foreach (var subscription in subscriptions) { subscription((o,e) => { var index = subscriptions.IndexOf(subscription); fired.Enqueue(index); }); }
但结果是相同的,触发包含{1; 1}而不是{1; 0}.
现在我想知道是否将同一个lambda分配给两个事件而不是使用不同的订阅/索引状态.有任何想法吗?
更新:到目前为止,我无法获得任何答案(与我的初始结果相同),尽管它们与我的实际代码相似,所以我认为问题位于我的FuelTank代码的其他位置.我在下面粘贴了FuelTank的完整代码:
public class FuelTank { public FuelTank() { } public FuelTank(float initialFuel,float maxFuel) { MaxFuel = maxFuel; Fuel = initialFuel; } public float Fuel { get { return fuel; } private set { var adjustedFuel = Math.Max(0,MaxFuel)); if (fuel != adjustedFuel) { var oldFuel = fuel; fuel = adjustedFuel; RaiseCheckFuelChangedEvents(oldFuel); } } } private float maxFuel; public float MaxFuel { get { return maxFuel; } set { if (value < 0) { throw new ArgumentOutOfRangeException("MaxFuel",value,"Argument must be not be less than 0."); } maxFuel = value; } } private float fuel; public event EventHandler<FuelEventArgs> FuelChanged; public event EventHandler FuelEmpty; public event EventHandler FuelFull; public event EventHandler FuelNoLongerEmpty; public event EventHandler FuelNoLongerFull; public void AddFuel(float fuel) { Fuel += fuel; } public void ClearFuel() { Fuel = 0; } public void DrainFuel(float fuel) { Fuel -= fuel; } public void FillFuel() { Fuel = MaxFuel; } private void RaiseCheckFuelChangedEvents(float oldFuel) { FuelChanged.FireEvent(this,EventArgs.Empty); } } }
FuelEventArgs看起来像这样:
public class FuelEventArgs : EventArgs { public float NewFuel { get; private set; } public float OldFuel { get; private set; } public FuelEventArgs(float oldFuel,float newFuel) { this.OldFuel = oldFuel; this.NewFuel = newFuel; } }
FireEvent扩展方法如下所示:
public static class EventHandlerExtensions { /// <summary> /// Fires the event. This method is thread safe. /// </summary> /// <param name="handler"> The handler. </param> /// <param name="sender"> Source of the event. </param> /// <param name="args"> The <see cref="EventArgs"/> instance containing the event data. </param> public static void FireEvent(this EventHandler handler,object sender,EventArgs args) { var handlerCopy = handler; if (handlerCopy != null) { handlerCopy(sender,args); } } /// <summary> /// Fires the event. This method is thread safe. /// </summary> /// <typeparam name="T"> The type of event args this handler has. </typeparam> /// <param name="handler"> The handler. </param> /// <param name="sender"> Source of the event. </param> /// <param name="args"> The <see cref="EventArgs"/> instance containing the event data. </param> public static void FireEvent<T>(this EventHandler<T> handler,T args) where T : EventArgs { var handlerCopy = handler; if (handlerCopy != null) { handlerCopy(sender,args); } } }
完整的测试代码可以在问题的上面找到,在测试执行期间没有其他代码被调用.
我通过Unity测试工具插件使用NUnit测试框架,Unity3D引擎,.NET版本3.5(是的,它更接近Mono 2.0,我相信)和Visual Studio 2013.
更新2:
在将代码和测试提取到他们自己的项目之后(在Unity3D生态系统之外),所有测试都按预期运行,因此我将不得不将这个问题归结为Unity中的错误 – > Visual Studio桥.
解决方法
首先是FuelTank的课程:
public class FuelTank { private float fuel; //Basic classes for the event handling,could be done by providing a few simple delegates,//but this is just to stick as close to the original question as possible. public FuelChanged FuelChanged = new FuelChanged(); public FuelEmpty FuelEmpty = new FuelEmpty(); public FuelFull FuelFull = new FuelFull(); public FuelNoLongerEmpty FuelNoLongerEmpty = new FuelNoLongerEmpty(); public FuelNoLongerFull FuelNoLongerFull = new FuelNoLongerFull(); public float MaxFuel { get; set; } public float Fuel { get { return fuel; } private set { var adjustedFuel = Math.Max(0,EventArgs.Empty); } } }
由于缺少事件处理程序的代码,我假设使用它.正如评论在前面的代码块中所描述的那样,普通代表可以更轻松地完成.这只是一个选择问题,我认为这个实现不是最好的,但适合调试:
public class FuelEventArgs : EventArgs { private float oldFuel,newFuel; public FuelEventArgs(float oldFuel,float newFuel) { this.oldFuel = oldFuel; this.newFuel = newFuel; } } public class FuelEvents { public event EventHandler FireEventHandler; public virtual void FireEvent(object sender,EventArgs fuelArgs) { EventHandler handler = FireEventHandler; if (null != handler) handler(this,fuelArgs); } } public class FuelChanged : FuelEvents { public override void FireEvent(object sender,EventArgs fuelArgs) { Console.WriteLine("Fired FuelChanged"); base.FireEvent(sender,fuelArgs); } } public class FuelEmpty : FuelEvents { public override void FireEvent(object sender,EventArgs fuelArgs) { Console.WriteLine("Fired FuelEmpty"); base.FireEvent(sender,fuelArgs); } } public class FuelFull : FuelEvents { public override void FireEvent(object sender,EventArgs fuelArgs) { Console.WriteLine("Fired FuelFull"); base.FireEvent(sender,fuelArgs); } } public class FuelNoLongerEmpty : FuelEvents { public override void FireEvent(object sender,EventArgs fuelArgs) { Console.WriteLine("Fired FuelNoLongerEmpty"); base.FireEvent(sender,fuelArgs); } } public class FuelNoLongerFull : FuelEvents { public override void FireEvent(object sender,EventArgs fuelArgs) { Console.WriteLine("Fired FuelNoLongerFull"); base.FireEvent(sender,fuelArgs); } }
为了测试它,我使用了这个类,包含了原始问题中的大多数代码:
[TestFixture] public class Tests { public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Action triggerAction) { var expectedSequence = new Queue<int>(); for (int i = 0; i < subscribeActions.Count; i++) { expectedSequence.Enqueue(i); } ExpectEventSequence(subscribeActions,expectedSequence); } public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Queue<int> expectedSequence) { var fired = new Queue<int>(); var actionsCount = subscribeActions.Count; //This code has been commented out due to the fact that subscription is unknown here. //I stuck to use the last solution that Nick provided himself //for (var i = 0; i < actionsCount; i++) //{ // subscription((o,e) => // { // fired.Enqueue(i); // }); //} var subscriptions = subscribeActions.ToList(); foreach (var subscription in subscriptions) { subscription((o,e) => { var index = subscriptions.IndexOf(subscription); Console.WriteLine("[ExpectEventSequence] Found index: {0}",index); fired.Enqueue(index); }); } triggerAction(); var executionIndex = 0; var inOrder = true; foreach (var firedIndex in fired) { if (firedIndex != expectedSequence.Dequeue()) { inOrder = false; break; } executionIndex++; Console.WriteLine("Execution index: {0}",executionIndex); } if (subscribeActions.Count != fired.Count) { Assert.Fail("Not all events were fired."); } if (!inOrder) { Console.WriteLine("Contents of Fired Queue: {0}",PrintValues(fired)); Assert.Fail(string.Format( CultureInfo.CurrentCulture,executionIndex)); } } private static string PrintValues(Queue<int> myCollection) { return string.Format( "{{0}}",string.Join(",",myCollection.ToArray())); } [Test()] [ExpectedException(typeof(DivideByZeroException))] public void FillFuel_Test([Values(1,100)]float maxFuel) { var fuelTank = new FuelTank() { MaxFuel = maxFuel }; var eventHandlerSequence = new Queue<Action<EventHandler>>(); eventHandlerSequence.Enqueue(x => fuelTank.FuelFull.FireEventHandler += x); //Dealing with a subclass of EventHandler eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged.FireEventHandler += (o,e)); ExpectEventSequence(eventHandlerSequence,() => fuelTank.FillFuel()); } }
现在,在使用NUnit运行测试时,我注意到以下结果:
触发的第一个事件是FuelChanged事件,这会导致方法中的已触发队列
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Queue<int> expectedSequence)
包含{1}.
触发的下一个事件是FuelFull事件,这意味着已触发的队列现在包含:
根据尼克的问题,正如预期的那样{1,0}.
触发的最后一个事件是FuelNoLongerEmpty事件,这个事件未通过测试.
注意:
由于这段代码还没有提供一个原始问题的答案,即lambda可能会引起一些干扰,正如我上面提供的代码所做的那样.
以下规则适用于lambda表达式中的变量范围:
>捕获的变量在收到之前不会被垃圾收集
引用它的委托超出范围.
> lambda表达式中引入的变量在中不可见
外部方法.
> lambda表达式无法直接捕获ref或out参数
从封闭的方法.
> lambda表达式中的return语句不会导致
封闭方法返回.
> lambda表达式不能包含goto语句,break语句,
或继续声明其目标在身体外或身体内
包含匿名函数.
因此,Nick的原始问题中的问题可能是由于您在队列中进行枚举.枚举并直接将它们传递给lambda表达式时,您将使用引用.一个技巧可能是通过将其复制到迭代循环范围内的局部变量来实际取消引用它.这正是smiech在他的帖子中提到的.
编辑:
我刚刚再次为你调查了一遍.你确定你所遇到的“挑战”不仅仅是将被解雇的词典的索引与expectedSequence进行比较.事件是否按逆序发生?请注意,队列是基于FIFO的,因此当出队时,它将检索插入的第一个…
我注意到(根据我的代码),触发的字典包含{1,0},而expectedSequence字典包含{0,1}.通过查看预期事件,这对expectedSequence队列很有用.实际上,通过事件处理程序的“年龄”,错误地构建了已触发的队列(填充在最后一个代码块中).
当我在原始代码中更改一个语句时
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions,Queue<int> expectedSequence)
方法来自
var subscriptions = subscribeActions.ToList(); foreach (var firedIndex in fired) { if (firedIndex != expectedSequence.Dequeue()) { inOrder = false; break; } executionIndex++; Console.WriteLine("Execution index: {0}",executionIndex); }
对此:
//When comparing indexes,you'll probably need to reverse the fired queue fired = new Queue<int>(fired.Reverse()); foreach (var firedIndex in fired) { if (firedIndex != expectedSequence.Dequeue()) { inOrder = false; break; } executionIndex++; Console.WriteLine("Execution index: {0}",executionIndex); }
然后测试中的所有内容都将完美无缺,正如您在此屏幕截图中看到的那样: