Swift: В чем разница между использованием типов как / как? / как! ключевые слова и C-стиль актерского состава? - PullRequest
7 голосов
/ 16 апреля 2019

Я заметил, что я могу привести замыкание с регулярными аргументами к замыканию, аргументы которого заключены в кортеж. Но только если я использую определенный метод приведения!

let myClosure = { (a: Int, b: Float) -> Void in
    print(a, b)
}

// I want to convert the closure to be of this type (I won't go into why).
var myClosureWithTupleArgVar: (((Int, Float)) -> Void)? = nil

// This cast is possible.
myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)
myClosureWithTupleArgVar?((1, 2))

// This cast will always fail and return nil (as warned by the compiler).
myClosureWithTupleArgVar = myClosure as? (((Int, Float)) -> Void)
myClosureWithTupleArgVar?((3, 4))

Выходы:

1 2.0

Почему это возможно? В чем разница между использованием as преобразования типов и вызова функций в стиле C?

(Меня не интересует разница между as, as? И as!)

Ответы [ 3 ]

7 голосов
/ 16 апреля 2019

Дополнительная скобка - причина, по которой перед этим предупреждением (несмотря на это, он показывает результат «1 2.0 3 4.0» на игровой площадке).

Сначала позвольте мне подтвердить, что:

он должен быть myClosureWithTupleArgVar?((1, 2)) вместо myClosureWithTupleArgVar?(1, 2)

, а также myClosureWithTupleArgVar?((3, 4)) вместо myClosureWithTupleArgVar?(3, 4)

Это потому, что тип myClosureWithTupleArgVar равен (((Int, Float)) -> Void)?.

По какой-то причине компилятор распознает, что (Int, Float) -> Void (тип myClosure) равен , а не так же, как ((Int, Float)) -> Void (тип myClosureWithTupleArgVar).На этом этапе, если вы попытались отредактировать свой код как:

let myClosure = { (a: Int, b: Float) -> Void in
    print(a, b)
}

var myClosureWithTupleArgVar: ((Int, Float) -> Void)? = nil

// This cast is possible.
myClosureWithTupleArgVar = ((Int, Float) -> Void)?(myClosure)
myClosureWithTupleArgVar?(1, 2)

myClosureWithTupleArgVar = myClosure as? ((Int, Float) -> Void)
myClosureWithTupleArgVar?(3, 4)

, удалив лишние скобки (((Int, Float) -> Void)? вместо (((Int, Float)) -> Void)), вы должны увидеть противоположное предупреждение!Что:

Условное приведение от '(Int, Float) -> Void' к '(Int, Float) -> Void' всегда завершается успешно

, что означает, чтовам даже не нужно больше упоминать as кастинг (у них сейчас точно такой же тип):

myClosureWithTupleArgVar = myClosure

вместо

myClosureWithTupleArgVar = myClosure as? ((Int, Float) -> Void)

Также:

myClosureWithTupleArgVar = myClosure

вместо:

myClosureWithTupleArgVar = ((Int, Float) -> Void)?(myClosure)

Имейте в виду, что этот случай не только для литья крышек.Пример:

let int1 = 100
var int2: Int? = nil

// unnecessary castings:
int2 = Int(int1) // nothing shown here, because of Int init: init(_ value: Int)
int2 = int1 as? Int // Conditional cast from 'Int' to 'Int' always succeeds
4 голосов
/ 28 мая 2019

Прежде всего, стоит отметить, что:

myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)

не является приведением в стиле C, так как приведение в Swift выполняется с помощью операторов as / as? / as!.

Скорее, это синтаксический сахар для:

myClosureWithTupleArgVar = Optional<((Int, Float)) -> Void>(myClosure)

, который является вызовом инициализатора Optional init(_ some: Wrapped).

Причина, по которой функцию (Int, Float) -> Void можно преобразовать в функцию ((Int, Float)) -> Void через инициализатор Optional, заключается в том, что Swift в настоящее время реализует преобразование специального аргумента, которое может преобразовать функцию с N параметрами в функцию, которая принимает одиночный N-элементный кортежный параметр.

Например:

typealias TupleTakingFn = ((Int, Float)) -> Void
func giveMeATupleTakingFn(_ fn: @escaping TupleTakingFn) -> TupleTakingFn {
  return fn
}

let myClosure = { (a: Int, b: Float) -> Void in
  print(a, b)
}

let myClosureWithTupleArgVar = giveMeATupleTakingFn(myClosure)

Это преобразование выполняется только как особый случай преобразования аргумента, который может быть принят только при передаче аргумента параметру. Поэтому его нельзя использовать в других случаях, таких как прямое назначение или приведение типа через as / as / as!, поэтому ваше приведение не удается.

Хорошо, но почему существует это специальное преобразование? Ну, в Swift 3 вы имели обыкновение свободно преобразовывать функцию N-арности в функцию получения кортежа из N элементов (и наоборот), что позволяет компилировать следующее:

func foo(_ x: Int, _ y: Int) {}
func bar(_ x: (Int, Int)) {}

let fn1: (Int, Int) -> Void = bar   // ✅
let fn2: ((Int, Int)) -> Void = foo // ✅

Это был остаток от разбрызгивания кортежа, удаленный с помощью SE-0029 , и поэтому он был удален в Swift 4 с помощью SE-0110 , что означает, что функция N-арности может теперь используется только там, где ожидается функция N-арности:

let fn1: (Int, Int) -> Void = bar   // ❌
let fn2: ((Int, Int)) -> Void = foo // ❌

Однако это изменение вызвало регрессию юзабилити, когда следующий пример (среди прочего) больше не будет компилироваться:

let dict = [5: ""]
let result = dict.map { key, value in
  (key + 1, value + "hello")
}

Это связано с тем, что мы пытаемся передать замыкание с двумя параметрами в метод map(_:), который ожидает функцию типа (Element) -> T, которая в случае Dictionary is ((key: Key, value: Value)) -> T.

Чтобы устранить эту регрессию и сохранить приведенный выше код легальным, преобразование из функции N-арности в функцию приема кортежа из N * элементов было повторно введено, но только в особом случае для преобразования аргументов :

[W] e «отменит» изменение SE-0110 в отношении аргументов функции от Swift 4.

В частности, при передаче значения аргумента типа функции (включая замыкания) для параметра типа функции, многопараметрическая функция аргумента может быть передана параметру, Тип функции принимает один кортеж (чьи элементы кортежа соответствуют типы параметров функции аргумента).

Таким образом, приведенный выше пример остается легальным при запрете таких преобразований функций в других местах Однако он также имеет неприятный побочный эффект: ваш пример:

myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)

для компиляции.

3 голосов
/ 27 мая 2019

Краткий ответ:

Приведение типа C в основном означает, что компилятор Swift просто заставит ваше замыкание вызываться так, как если бы он принимал кортеж (Int, Float) в качестве параметра, тогда как as / as?/ как!бросок сначала сделает некоторые проверки работоспособности на вашем броске, чтобы гарантировать, что типы совместимы и так далее.

Поскольку компилятор считает (в некоторых версиях, как видно из комментариев к другому ответу), что (Int, Float) -> () и ((Int, Float)) -> () слишком далеко друг от друга, чтобы быть совместимыми, проверка работоспособности просто вернет ноль, поэтомублокировка вашего вызова.

Что делает его работающим, так это то, что функция / замыкание, принимающее кортеж (Int, Float), ведет себя точно так же (в текущей версии Swift), что и функция / замыкание, принимающее Int иFloat параметр.

Длинный ответ:

Я собрал фрагмент кода в сборку, на которую я буду ссылаться с этого момента.Этот фрагмент можно найти здесь: https://swift.godbolt.org/z/CaOb0s

Для удобства чтения я использовал здесь функции вместо реальных замыканий.

Я создал две функции, соответствующие двум имеющимся у нас случаям:

func twoParamFunc(a: Int, b: Float)-> Void {
    print(a, b)
}

func singleParamFunc(tuple: (a: Int, b: Float))-> Void {
    print(tuple.a, tuple.b)
}

Затем я попытался привести их, используя два разных метода:

    let cCastFunction = ((((Int, Float)) -> Void)?(twoParamFunc))!
    let asCastFunction = (twoParamFunc as? (((Int, Float)) -> Void))!

И, глядя на код сборки, скомпилированный с помощью swift, мы можем увидеть много различий между этими двумя,

Рассматривая приведение в стиле C, мы видим, что большая часть кода в основном просто вызывает alloc / retain / release и перемещает указатели и значения вокруг.Единственный вызов внешнего кода происходит через случай сбоя (разыменование ! пустой ссылки), вызывая $ss18_fatalErrorMessage__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF

В то время как в приведении в стиле swift, есть много дополнительных вызовов (здравомыслиечеки, о которых я говорил ранее).У нас есть, например,

call    (type metadata accessor for (Swift.Int, Swift.Float) -> ())
...
call    (type metadata accessor for ((Swift.Int, Swift.Float)) -> ())
...
call    swift_dynamicCast@PLT

, который ясно показывает, что компилятор Swift выполняет некоторые проверки совместимости типов приведения и нигде не встречается в приведении в стиле c.

Так что теперь, когда была найдена разница между приведением типов в стиле C и Swift, мы можем попытаться понять, почему работает вызов приведенной функции стиля C.

При просмотре сгенерированного кода сборкис помощью двух простых вызовов функций, которые я сделал в образце:

    twoParamFunc(a: a.0,b: a.1)
    singleParamFunc(tuple: a)

Мы можем видеть, что эти функции на самом деле скомпилированы так, чтобы вызываться одинаково:

singleParamFunc:

        mov     rdi, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
        movss   xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
        call    (output.singleParamFunc(tuple: (a: Swift.Int, b: Swift.Float)) -> ())

Здесь мы видим, что значение, соответствующее первому значению кортежа, заносится в регистр rdi, а второе - в xmm0, а затем функция вызывается

* 1053.*twoParamFunc:
        mov     rax, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
        movss   xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
...
        mov     rdi, rax
...
        call    (output.twoParamFunc(a: Swift.Int, b: Swift.Float) -> ())

В этой функции это не так просто, но теперь значение 1 заносится в регистр rax, который сам копируется в регистр rdi, а значение 2 все еще входит в регистрxmm0 и вызывается функция.

Но в этом примере, поскольку мы занимаемся другими вещами, ассемблерный код немного запутан, я сделал еще один пример для его проверки: https://swift.godbolt.org/z/vDCZZV

В этом примере (к которому я добавил еще один тест со структурой) мы видим, что код сборки, созданный для вызова трех функций, абсолютно одинаков:

        mov     rdi, qword ptr [rip + (output.structValue : output.struct_test)]
        movss   xmm0, dword ptr [rip + (output.structValue : output.struct_test)+8]
        call    (output.test(value: output.struct_test) -> ())
        mov     rdi, qword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))]
        movss   xmm0, dword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))+8]
        call    (output.test2(tuple: (Swift.Int, Swift.Float)) -> ())
        mov     ecx, 1
        mov     edi, ecx
        movss   xmm0, dword ptr [rip + .LCPI0_0]
        call    (output.test3(a: Swift.Int, b: Swift.Float) -> ())

Чтобы возобновить, в текущей версии swift, любая из этих трех функций может быть c-приведена к любой другой и все еще работает.

Это оказалось намного дольше, чем первоначально планировалось, но я подумал, что этопроблема это заслужила.

...