InvalidOperationException集合已被修改,但我在迭代时没有修改它?

时间:2016-08-07 13:51:31

标签: .net exception collections f#

我目前在F#中编写一个小型服务器类型,它基本上存储了与之有效的TCP连接。在它的循环中,它检查新连接,并使用以下代码检查当前连接是否仍然存在:

type TCPListenerServer(discoveryPort:int) =
    let server = new TcpListener (IPAddress.Loopback, discoveryPort)

    let activeConnections = new List<TcpClient>()
    let cancellationToken = new CancellationTokenSource()

    let connectionIsStillActive (client:TcpClient) =
        let ipProperties = IPGlobalProperties.GetIPGlobalProperties ()
        let allTcpConnections = ipProperties.GetActiveTcpConnections ()
        let relevantTcpConnections = Array.filter (fun (connectionInfo:TcpConnectionInformation) -> 
            (connectionInfo.LocalEndPoint = (client.Client.LocalEndPoint :?> IPEndPoint)) && (connectionInfo.RemoteEndPoint = (client.Client.RemoteEndPoint :?> IPEndPoint))) allTcpConnections

        try
            let stateOfConnection = (Array.get relevantTcpConnections 0).State
            match stateOfConnection with
            | TcpState.Established ->
                true
            | _ ->
                false
        with
        | :? System.IndexOutOfRangeException as ex ->
            false

    let rec loop (pendingConnection:Task<TcpClient>) = async {            
        let newPendingConnection, client =
            match pendingConnection.Status with
            | TaskStatus.Created | TaskStatus.WaitingForActivation | TaskStatus.WaitingToRun 
            | TaskStatus.WaitingForChildrenToComplete | TaskStatus.Running  ->
                (None, None)
            | TaskStatus.Faulted ->
                let result = pendingConnection.Exception
                raise (new System.NotImplementedException())
            | TaskStatus.Canceled ->
                raise (new System.NotImplementedException())
            | TaskStatus.RanToCompletion ->
                let connectionTask = server.AcceptTcpClientAsync ()
                (Some connectionTask, Some pendingConnection.Result)
            | _ -> 
                raise (new System.NotImplementedException())

        // Add the new client to the list
        Option.iter (fun c -> activeConnections.Add c) client

        // Switch the new pending connection if there is one
        let connectionAttempt = defaultArg newPendingConnection pendingConnection

        // Check that the connections are still alive
        let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections
        Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove  // <-- Exception happens here


        Async.Sleep 1000 |> Async.RunSynchronously
        return! loop connectionAttempt
    }

    member x.Start () =
        if not cancellationToken.Token.CanBeCanceled
        then
            raise (new System.Exception("Cancellation token cannot be used to cancel server loop task."))

        try
            server.Start ()
            let connectionTask = server.AcceptTcpClientAsync ()
            Async.Start (loop connectionTask, cancellationToken.Token)
        with 
        | :? SocketException as ex ->
            server.Stop ()
            raise ex

    member x.Stop () =
        cancellationToken.Cancel ()
        Async.Sleep 2000 |> Async.RunSynchronously
        server.Stop ()
        activeConnections.Clear ()

    member x.ActiveConnections =
        activeConnections

为了看看这是否运作良好,我实施了这个简单的单元测试:

[<TestMethod>]
[<TestCategory(Networking)>]
member x.``Start Server, Client Connects, then Disconnects`` () =
    let server = new TCPListenerServer(44000)
    server.Start ()

    let client = createClientAndConnect 44000

    Async.Sleep 5000 |> Async.RunSynchronously

    client.GetStream().Close()
    client.Close()

    Async.Sleep 5000 |> Async.RunSynchronously

    Assert.IsTrue(server.ActiveConnections.Count = 0, "There are still connections in the server's active connections list.")

    cleanupTest server (Some [client])

不幸的是,当我运行此测试时,一旦在客户端上调用Close(),我就会收到以下异常:

  

System.InvalidOperationException:修改了集合;枚举操作可能无法执行。      在System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource资源)      在System.Collections.Generic.List 1.Enumerator.MoveNextRare() at System.Collections.Generic.List 1.Enumerator.MoveNext()      在Microsoft.FSharp.Collections.IEnumerator.next@224 [T](FSharpFunc 2 f, IEnumerator 1 e,FSharpRef 1 started, Unit unitVar0) at Microsoft.FSharp.Collections.IEnumerator.filter@219.System-Collections-IEnumerator-MoveNext() at Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc 2动作,IEnumerable 1 source) at TCPListenerServer.clo@35.Invoke(Unit unitVar) in E:\Documents\Source Control Projects\WiDroid\Core.Networking\TCPListenerServer.fs:line 61 at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@851.Invoke(AsyncParams 1 args)

我知道当您在迭代时修改集合时会发生此异常。但我不能直接在我的代码中这样做。我想知道为什么我得到这个例外?

1 个答案:

答案 0 :(得分:3)

let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections
Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove

我认为问题出在那里,Seq.filter返回一个seq(IEnumerable<T>的别名)。 代表的不是某种数据容器(如数组或列表),而是一种计算,它会在某些时候给出项目&#34;。

这意味着当你执行Seq.iter时,它必须&#34;计算&#34; connectionsToRemove有效地迭代activeConnections。但是在同一个Seq.iter中,你要求从迭代集合中删除项目,因此错误。

如果您熟悉C#,那么代码可以看起来就行,然后您就会清楚地看到问题

foreach (TcpClient connection in activeConnections)
{
    if (!connectionIsStillActive connection)
    {
        activeConnections.Remove (connection);
    }
}

您可以通过拨打List<T>.RemoveAll

来替换它
//doesn't seems to work directly (can't convert (TcpClient -> bool) to Predicate<TcpClient>)
//activeConnections.RemoveAll (not << connectionIsStillActive)

//this would work though
//activeConnections.RemoveAll (Predicate (not << connectionIsStillActive))

activeConnections.RemoveAll (fun connection -> not <| connectionIsStillActive connection)
|> ignore