通过计算属性或下标进行Swift链式赋值:这在哪里记录了?

时间:2016-01-15 22:13:48

标签: swift swift2

我很惊讶地发现,通过Swift中的下标操作或计算属性来分配值类型的成员,就像对参考类型所期望的那样:我真的期望myArrayOfValueType[0].someField = value将被禁止或禁止操作,因为它只会分配给被丢弃的副本。但实际上它所做的是调用getter setter:执行变异然后自动返回值类型。

我的问题是:这种行为记录在哪里?我们能依靠这种行为吗?

struct Foo {
    var a : Int = 1
}
struct FooHolder {
    var foo :  Foo = Foo()

    var afoo : Foo {
        get { return foo }
        set { foo = newValue }
    }
    subscript(i: Int) -> Foo {
        get { return foo }
        set { foo = newValue }
    }
}
var fh = FooHolder()
fh.afoo.a // 1
fh.afoo.a = 42 // equivalent: var foo = fh.afoo; foo.a = 42; fo.afoo = foo
fh.afoo.a // 42!

// same is true of subscripts
var fh = FooHolder()
fh[0].a // 1
fh[0].a = 42
fh[0].a // 42!

编辑:

以另一种方式陈述问题:

就值类型复制而言,Swift使下标和计算属性访问看起来都是透明的。如果常规方法调用将返回值类型的副本,则Swift似乎在评估的不同阶段使用getter和setter执行两步舞蹈以生成值,对其进行修改,然后将其设置回来。这似乎应该被记录下来,如果没有其他原因,它完全不明显,甚至可能在编写不良的代码中产生副作用。

2 个答案:

答案 0 :(得分:1)

我很难找到这种行为的明确文档。在Swift一书的Computed Properties中有一点暗示,但可能更值得提前说明。我建议filing a bug反对文档。

与此同时 - 是的,你可以依赖这种行为,因为它似乎对Swift的设计非常重要。你可以从关于Swift设计理念的两个非常安全的假设中得到这个:

  1. 基本值类型数据结构应与其他C系列语言一样可用和可访问。
  2. 来自调用者视角的计算属性应与存储属性相同。
  3. 对于#1,请考虑以下(Obj)C示例:

    CGRect rect = CGRectMake(0, 0, 320, 480);
    // CGRect is a nested structure: {origin: {x, y}, size: {w, h}}
    rect.origin.y = 20;
    rect.size.height = 460;
    // rect is now {{0, 0}, {320, 460}}
    

    这是有效的,因为C结构是指针数学的语法糖。 CGRect实际上只是四个浮点值的连续块。 (作为嵌套结构,它是两个较小块的块,它们本身就是值块。)当您读取或写入origin.y时,编译器使用结构的定义来确定内存块中的读取位置或写一个浮点数,无论是在堆栈上静态分配的内存(函数参数或局部变量)还是在堆上动态分配。

    Swift需要能够处理源自或传递给C API的数据结构,因此您可以期望基本值类型结构与C中的工作方式非常相似,模拟var vs的可变性限制let。考虑扩展CGRect示例以涉及一系列rects:

    CGRect rects[10] = /*...*/;
    rects[5].size.height = 23;    
    

    同样,这在C语言中“正常工作”,因为它只是编译器为您处理的指针算法。 rects是一块连续的记忆;找到第6个子块的块(一个矩形/四个浮点块)的偏移量;找到size字段的子块(两个浮点数的大小/块)的偏移量;找到height字段的 的偏移量;写到那个位置。 Swift需要与C进行互操作(更不用说内存高效本身)了,所以这在Swift中也“正常”。

    对于#2,在Swift书中的Computed Properties之间的行之间进行读取非常强烈地暗示计算属性应该像调用者的角度来看一样存储属性。

    借用他们的例子,让我们扩展CGRect来添加一个计算的center属性:

    var center: CGPoint {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return CGPoint(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
    

    如果我们在Swift中有CGRect的数组,我们应该能够设置center,就像我们设置origin一样:

    rects[4].origin.x = 3
    rects[6].center.x = 5
    

    事实上,这确实有效。从调用者的角度来看,center只是另一个属性 - 他们不必关心它是存储还是计算。

    这是抽象的关键部分 :协议只会声明这些属性存在(并且是readwrite或readonly),并且采用该协议的两个不同结构可以实现存储的center和计算的origin,反之亦然。

    如何它的工作原理是Swift编译器比C编译器做的更多,但具有相同的理念。在C编译器看到对结构成员的访问并执行指针数学运算的情况下,Swift编译器会看到访问并插入通过指针间接使用底层值工作的函数调用。就好像C中存在以下功能一样:

    inline CGPoint CGRectGetCenter(CGRect rect) {
        return CGPointMake(rect.origin.x + (size.width / 2),
                           rect.origin.y + (size.height / 2)
    }
    inline void CGRectSetCenter(CGRect *rect, CGPoint newCenter) {
        rect->origin.x = newCenter.x - (rect->size.width / 2);
        rect->origin.y = newCenter.y - (rect->size.height / 2);
    }
    

    ...编译器自动将对rect.center的读写操作转换为对这些函数的调用:

    CGRect rect = CGRectMake(0, 0, 320, 480);
    CGRectGetCenter(rect); // {160, 240}
    CGRectSetCenter(&rect, CGPointMake(0, 0));
    // rect is now {{-160, -240}, {320, 480}}
    

    (请注意,在C中,无论传递的rect或指向rect的指针是在数组还是嵌套结构中,这些函数都能正常工作。)

    关于Swift的“神奇”部分是它一直向下应用这样的转换 - 所以如果一个struct包含另一个struct,那么通过函数重新定向计算属性访问器会一直向下运行,因此计算属性就像从调用者的角度来看存储的属性。

    它甚至适用于inout个函数参数。 +=和(虽然它仍在)++运算符是具有inout参数的函数,因此您可以执行以下操作:

    rects[7].origin.y += 10
    rect.center.x++
    

    每次使用带inout参数的函数时,编译器都会发出必要的指针数学或函数调用来读取成员的当前值,调用函数,然后执行反向的指针数学运算/ function调用将结果放在适当的位置。因此,rect.center.x++调用CGRect.center.get,在生成的CGPoint结构中调用一个值,然后调用CGRect.center.set

    (这部分在Swift一书的In-Out BurgersParameters中有更详尽的记录。)

答案 1 :(得分:0)

默认情况下复制值类型

struct Foo {
    var a : Int = 1
}
struct FooHolder {
    var foo :  Foo = Foo()
    var afoo : Foo {
        get { return foo }
        set { foo = newValue }
    }
}
var fh = FooHolder()

// arr1 and arr2 DOESN'T share the same copy of fh
var arr1 = [fh]
arr1[0].foo.a = 0
var arr2 = [fh]
arr2[0].foo.a = 200

// foo is a new copy of arr1[0].afoo (Foo)
var foo = arr1[0].afoo // Foo(a: 1)
foo.a = 100
print(arr1,arr2,fh,foo)
// [FooHolder(foo: Foo(a: 0))] [FooHolder(foo: Foo(a: 200))] FooHolder(foo: Foo(a: 1)) Foo(a: 100)

arr1[0].foo = foo
print(arr1,arr2,fh,foo)
// [FooHolder(foo: Foo(a: 100))] [FooHolder(foo: Foo(a: 200))] FooHolder(foo: Foo(a: 1)) Foo(a: 100)

如果您有不同的行为,请检查Swift安装(版本,操作系统......)并填写雷达。

看到它按预期工作,请参阅下一个示例

protocol P {
    var i: Int {get set}
}
struct S:P {
    var i:Int
}
class C:P {
    var i: Int
    init(i:Int){
        self.i = i
    }
}

var s = S(i: 0)
var c = C(i: 0)
var arr1:[P] = [s,c]
var arr2:[P] = [s,c]
arr2[0].i = 20
arr2[1].i = 200
dump(arr1)
/*
▿ 2 elements
  ▿ [0]: S
    - i: 0
  ▿ [1]: C #0
    - i: 200
*/
dump(arr2)
/*
▿ 2 elements
  ▿ [0]: S
    - i: 20
  ▿ [1]: C #0
    - i: 200
*/

这是Swift语言定义的一部分,在许多地方的苹果文档中提到过,这种行为必须独立于Swift的平台。