swiftswift-concurrency

Actor isolation and non-sendable closure (use higher-order function inside Actor)


How to adjust forEachAsync for use within TestActor without a warning? It should stay reusable in other places.

I need TestActor to pass around non-sendable stuff within it. And I want to use higher-order convenience functions like forEachAsync (just an example) within it.

Build Settings -> Strict Concurrency Checking: Complete

class NotSendable { }

actor TestActor {
    func foo() async {
        let nonSendables = [NotSendable(), NotSendable()]

        // No problem
        for notSendable in nonSendables {
            await bar(notSendable)
        }

        // Warning: Sending 'nonSendables' risks causing data races
        await nonSendables.forEachAsync{ notSendable in
            await bar(notSendable)
        }
    }
    
    private func bar(_: NotSendable) async { }
}

extension Sequence {
    // Just an example for a higher-order async func
    func forEachAsync(_ operation: (Element) async -> Void) async {
        for element in self {
            await operation(element)
        }
    }
}

Solution

  • Swift 6.2 (Xcode 26)

    Enable Build Settings -> Approachable Concurrency. This is enabled by default for new projects. Thank you, Apple.

    Or enable only Build Settings -> nonisolated(nonsending) By Default. Approachable Concurrency enables this and others.

    Or, if you are worried about potentially changing the behavior of existing code, there's an explicit variant:

    extension Sequence {
        nonisolated(nonsending) func forEachAsync(
            _ operation: nonisolated(nonsending) (Element) async -> Void
        ) async {
            for element in self {
                await operation(element)
            }
        }
    }
    

    Swift packages:

    Use swift-tools-version 6.2 and add the NonisolatedNonsendingByDefault upcoming feature flag:

    // swift-tools-version: 6.2
    
    import PackageDescription
    
    let package = Package(
        ...
        targets: [
            .target(
                ...
                swiftSettings: [
                    .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
                    .enableUpcomingFeature("InferIsolatedConformances") // Optional
                ]
            )
        ]
    )
    

    Sources:

    Swift 6.0.3 (Xcode 16.2)

    With Swift Language Version set to Swift 6 in the Build Settings:

    class NotSendable { }
    
    actor TestActor {
        func foo() async {
            let nonSendables = [NotSendable(), NotSendable()]
            
            await nonSendables.forEachAsync{ notSendable, isolation in
                await bar(notSendable, isolation)
            }
        }
        
        private func bar(
            _: NotSendable,
            _ isolation: isolated (any Actor)? = #isolation
        ) async {
        }
    }
    
    extension Sequence {
        func forEachAsync(
            _ operation: (Element, isolated (any Actor)?) async -> Void,
            isolation: isolated (any Actor)? = #isolation
        ) async {
            for element in self {
                await operation(element, isolation)
            }
        }
    }
    

    You can still use forEachAsync outside of an Actor, in which case isolation is nil.

    Sources: