Intended behavior or bug in Dictionary's new subscript(_:default:)?


(Jens Persson) #1

// Swift 4, Xcode 9 beta 1, default toolchain

import Foundation

var d1 = [Int : String]()
d1[1, default: .init()].append("a")
d1[2, default: .init()].append("b")
d1[3, default: .init()].append("c")
d1[1, default: .init()].append("d")
print(d1) // [2: "b", 3: "c", 1: "ad"] as expected.

var d2 = [Int : NSMutableString]()
d2[1, default: .init()].append("a")
d2[2, default: .init()].append("b")
d2[3, default: .init()].append("c")
d2[1, default: .init()].append("d")
print(d2) // [:] but why?

I know that NSMutableString is a reference type and String is a value type
and that the default argument is an @autoclosure. I also know that the
newly created NSMutableString instance is just released immediately after
the append call, without being stored and retained in the Dictionary's
storage.

Is this the intended behavior and if so, please let me better understand
how/why.

/Jens


(Ben Cohen) #2

In order for this defaulting subscript technique to work as intended, the subscript { get } needs to be called, then the mutation happens, then the subscript { set } writes the mutated value back, including adding it for the first time it the default was needed.

Reference types, not being value types, skip the write-back part, because they shouldn’t need writing back – they should just get amended in place, because they’re reference types.

Except this particular technique is relying on it.

This is probably worth a bug report, though I’m not sure if there’s an easy fix. The alternative is that Dictionary.subscript(_: default:) be made a mutating get that sets the default if not present, even without the set. There’s downsides to this though: you would no longer be able to use this defaulting subscript with immutable dictionaries, and getting a default value would add it which might be very unexpected.

···

On Jun 16, 2017, at 1:40 PM, Jens Persson via swift-users <swift-users@swift.org> wrote:

// Swift 4, Xcode 9 beta 1, default toolchain

import Foundation

var d1 = [Int : String]()
d1[1, default: .init()].append("a")
d1[2, default: .init()].append("b")
d1[3, default: .init()].append("c")
d1[1, default: .init()].append("d")
print(d1) // [2: "b", 3: "c", 1: "ad"] as expected.

var d2 = [Int : NSMutableString]()
d2[1, default: .init()].append("a")
d2[2, default: .init()].append("b")
d2[3, default: .init()].append("c")
d2[1, default: .init()].append("d")
print(d2) // [:] but why?

I know that NSMutableString is a reference type and String is a value type and that the default argument is an @autoclosure. I also know that the newly created NSMutableString instance is just released immediately after the append call, without being stored and retained in the Dictionary's storage.

Is this the intended behavior and if so, please let me better understand how/why.

/Jens

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Jens Persson) #3

https://bugs.swift.org/browse/SR-5250

···

On Sun, Jun 18, 2017 at 6:12 AM, Ben Cohen <ben_cohen@apple.com> wrote:

In order for this defaulting subscript technique to work as intended, the
subscript { get } needs to be called, then the mutation happens, then the
subscript { set } writes the mutated value back, including adding it for
the first time it the default was needed.

Reference types, not being value types, skip the write-back part, because
they shouldn’t need writing back – they should just get amended in place,
because they’re reference types.

Except this particular technique is relying on it.

This is probably worth a bug report, though I’m not sure if there’s an
easy fix. The alternative is that Dictionary.subscript(_: default:) be made
a mutating get that sets the default if not present, even without the set.
There’s downsides to this though: you would no longer be able to use this
defaulting subscript with immutable dictionaries, and getting a default
value would add it which might be very unexpected.

> On Jun 16, 2017, at 1:40 PM, Jens Persson via swift-users < > swift-users@swift.org> wrote:
>
> // Swift 4, Xcode 9 beta 1, default toolchain
>
> import Foundation
>
> var d1 = [Int : String]()
> d1[1, default: .init()].append("a")
> d1[2, default: .init()].append("b")
> d1[3, default: .init()].append("c")
> d1[1, default: .init()].append("d")
> print(d1) // [2: "b", 3: "c", 1: "ad"] as expected.
>
> var d2 = [Int : NSMutableString]()
> d2[1, default: .init()].append("a")
> d2[2, default: .init()].append("b")
> d2[3, default: .init()].append("c")
> d2[1, default: .init()].append("d")
> print(d2) // [:] but why?
>
> I know that NSMutableString is a reference type and String is a value
type and that the default argument is an @autoclosure. I also know that the
newly created NSMutableString instance is just released immediately after
the append call, without being stored and retained in the Dictionary's
storage.
>
> Is this the intended behavior and if so, please let me better understand
how/why.
>
> /Jens
>
> _______________________________________________
> swift-users mailing list
> swift-users@swift.org
> https://lists.swift.org/mailman/listinfo/swift-users


(David Sweeris) #4

We could say "extension Dictionary where Value: class", but that'll only fix it when the compiler knows `Value` has reference semantics (and lead to even more confusion when used in generic functions without the "T: class" part).

Could we check if `Value: class` within the existing setter? Or if that's what we already do to skip the write-back, skip the skipping when one of the values is nil?

- Dave Sweeris

···

On Jun 17, 2017, at 21:12, Ben Cohen via swift-users <swift-users@swift.org> wrote:

In order for this defaulting subscript technique to work as intended, the subscript { get } needs to be called, then the mutation happens, then the subscript { set } writes the mutated value back, including adding it for the first time it the default was needed.

Reference types, not being value types, skip the write-back part, because they shouldn’t need writing back – they should just get amended in place, because they’re reference types.

Except this particular technique is relying on it.

This is probably worth a bug report, though I’m not sure if there’s an easy fix. The alternative is that Dictionary.subscript(_: default:) be made a mutating get that sets the default if not present, even without the set. There’s downsides to this though: you would no longer be able to use this defaulting subscript with immutable dictionaries, and getting a default value would add it which might be very unexpected.