Дополнение к Лекция 2 — особенности кода Calculator в Swift 1.2 и Swift 2.0 или как не «застрять» на этом месте

Если вы приступаете к изучению  Лекции 2 (1-ая часть, 2-ая часть и 3-я часть) с загруженными Swift 1.2  и Xcode 6.3, а не Swift 1.1  и Xcode 6.2, как у профессора Пола Хэгерти, то ближе к концу лекции (конец 2-ой части) вы получите ошибку, которой у профессора нет. Это может сильно «притормозить» ваш процесс обучения.  Для того, чтобы этого не произошло, я расскажу, чем отличается ситуация, которую демонстрирует профессор, в Swift 1.1 и в Swift 1.2. Профессор Пола Хэгерти хочет показать такую возможность  Swift как перегрузка (overloading) функций с одинаковыми именами на примере функций performOperation для операций с одним и двумя аргументами

Screen Shot 2015-02-03 at 2.29.20 PM
Его «задумка» в Swift 1.1 сработает, и он не получит никакой ошибки, а вы в Swift 1.2 — получите ошибку

 Method 'perform' with Objective-C selector 'perform: ' conflicts with previous declaration with the same Objective-C selector

которая говорит о том, что «Метод perform c Objective-C селектором perform: конфликтует с предыдущим определением того же самого Objective-C селектора».

Screen Shot 2015-05-17 at 2.23.09 PM
Позвольте, ну причем здесь Objective-C? Я пишу на Swift! А в Swift разрешена перегрузка функций. Разъяснение дается в Xcode Release Notes для Xcode 6.3 и Swift 1.2.

«Теперь Swift обнаруживает несоответствие между перегрузкой (overloading) и переопределением (overriding) в системе типов Swift и эффективным поведением, рассматриваемым на этапе ObjectiveC runtime.

Например, диагностируется следующий конфликт между ObjectiveC setter для “property” в этом классе и методом “setProperty” в его расширении»:

[objc]
class A : NSObject {
var property: String = "Hello" //Objective-C метод ‘setProperty:’
// уже декларирован setter для свойства’property’
}
extension A {
func setProperty(str: String) { } // ошибка: метод ‘setProperty’
// заново декларирует Objective-C метод ‘setProperty:’
}
[/objc]

И все-таки непонятно, откуда взялся Objective-C?
Все дело в том, что наш класс ViewController, который мы создали для интерфейса калькулятора, наследует от Cocoa класса UIViewCiontroller

Screen Shot 2015-05-17 at 6.16.22 PM

 

Классам, которые является совместимыми с ObjectiveC объектами (такие классы используют @objc тэг или наследуют от существующих Cocoa классов, таких  как NSObject или UIView), запрещено использовать перегрузку ( overloading) функций, так как они передаются на этапе runtime компилятору  ObjectiveC, который вообще не поддерживает перегрузку ( overloading)  функций.

Для устранения этой ошибки рекомендуют использовать внутри класса типы, которые не поддерживаются ObjectiveC, тогда это предотвратит передачу класса компилятору ObjectiveC на стадии runtime, а заставит функционировать компилятор Swift, в котором overloading (перегрузка с одинаковыми именами) функций разрешена.

Наиболее простой способ этого добиться — сделать хотя бы один из методов  performOperation private.

Screen Shot 2015-05-17 at 5.50.32 PM
Надо сказать, что в Лекции 3 мы перенесем все «вычислительные» методы в специальный класс CalculatorBrain, который является базовым и ни от чего не наследует. Там нет ограничений ни на перегрузку (overloading), ни на переопределение (overriding) функций, так что дальше мы с этой проблемой не встретимся.

Однако проблема не исчезла, она есть и сохранится в Swift 2.0.

На WWDC 2015 на сессия 401 «Swift and Objective-С Interoperability» (Взаимодействие Swift и Objective-C)  на 6-ой минуте рассматривается именно наш пример с калькулятором и двумя методами с одинаковыми именами, но  разными типами входных параметров.

Эта проблема связа с вопросом: «Когда Swift выставляет мой класс для использования в Objective-C?» То есть, когда Swift неявно используется атрибут @objc для всех методов и свойств моего класса? Компилятор Swift делает это автоматически вместо вас. В качестве ответа на этот вопрос я перевожу отрывок из  WWDC 2015 сессии 401, ссылка на которую указана выше.

. . . . . . . . . . .

Давайте пройдемся по правилам представления  Swift кода  Objective, которые выполняются по умолчанию.

Если у вас есть класс , который является subclass NSObject, то по умолчанию, все его методы и свойства неявно выставляются компилятором  Swift для использования в Objective-C.

Если вы пометите метод как private, что означает видимость этого метода только в пределах текущего файла, то Swift не будет выставлять этот метод Objective-C по умолчанию.

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

Наконец, если вы не subclass NSObject, но просто подтверждаете Objective-C протокол, то вы должны очень внимательно отнестись к тому, чтобы ваши методы удовлетворяли всем требованиям протокола. И в Xcode 7 вы получите предупреждение о том, что этот метод является non-objc и не удовлетворяет требованиям @objc протокола.
Screen Shot 2015-06-21 at 5.33.17 PM
Сказанное до сих пор относится к случаю по умолчанию.
Что произойдет, если вы захотите изменить поведение выставления  Swift кода  Objective-С по умолчанию?
Если вы хотите выставить какой-то особенный метод или свойство Objective-C, вам нужно использовать старый добрый атрибут @objc без какой-либо дополнительной семантики.
Все эти правила вы можете прочитать в руководстве «Using Swift with Cocoa and Objective-C», которое доступно на developer.apple.com/Swift.

Итак, мы разобрали, как ведет себя Swift по умолчанию при выставлении классов, свойств и методов  Objective-C,  мы разобрали, что нужно сделать, чтобы переопределить поведение по умолчанию и сделать некоторые вещи более ObjC.
А как нам быть, если нужно решить противоположную проблему? Давайте посмотрим на класс СalculatorСontroller.
Screen Shot 2015-06-21 at 10.11.33 PM

У него два метода с именем performOperation. Один из этих методов берет один тип замыкания, другой метод — другой тип замыкания. Это нормально. Swift в состоянии различить эти методы, просто основываясь на различных типах аргумента. Но Objective-C так не работает. В Objective-C методы различаются только по именам, не по типам.
В нашем случае Swift знает, что это может быть ошибка и он дает вам сообщение об ошибке.
Screen Shot 2015-06-21 at 10.53.57 PM
Теперь мы вынуждены всегда исправлять эту ошибку, используя, например, @objc атрибут в форме селектора.
Screen Shot 2015-06-21 at 11.02.12 PM
Написав этот код, я переименовал нижний метод в performBinaryOperation, если я буду использовать его в Objective-C. Верхний метод все еще называется performOperation.

Примечание. Мы знаем, что эту проблему можно решить проще, сделав один или оба метода private, и тем самым предотвратив неявное появление атрибута @objc  у соответствующего метода.

В Xcode 7 мы добавили другую опцию для разрешения этой проблемы, которая представляет собой атрибут @nonobjc .
Screen Shot 2015-06-21 at 11.08.51 PM
Как можно ожидать из названия, берется то, что в нормальном режиме выставляется Objective-C и предотвращается это выставление  Objective-C.
Вы можете применять атрибут @nonobjc к любому методу, свойству, инициализатору или подписке (subscript).

Дополнение к Лекция 2 — особенности кода Calculator в Swift 1.2 и Swift 2.0 или как не «застрять» на этом месте: 5 комментариев

  1. Ну раньше то работало, у профессора, например.
    Без всяких этих костылей.
    В чем же соль, зачем было менять в худшую сторону?

    Не было такого вопроса на WWDC 2015 на сессия 401 ?

    • Во-первых, сессия 401 WWDC 2015 5-ая минута и далее — рассматривается именно наш калькулятор. Если вы посмотрите внимательно на класс ViewController, то увидите, что он наследует от UIViewController, а в конечном итоге от NSObject, что резко повышает вероятность использования его в Objective-C, поэтому в Swift 1.2 (а профессор читал лекции для Swift 1.0, а сейчас Swift 2.2 и 3.0 на подходе) было решено, что по умолчанию все методы в классе, который в конечном итоге наследует от NSObject, рассматривать как представимые в Objective-C без каких-либо дополнительных «пометок». Если же в методе присутствует синтаксис, специфический для Swift, то этот метод не представим в Objective-C. Однако каждый метод (performOperation) в отдельности может быть представим в Objective-C, но наличие нескольких методов с одинаковыми имена, но разной сигнатурой допустимо только в Swift. Именно об этом компилятор вас и предупреждает и просит указать явно, что эти методы не будут экспонироваться в Objective-C.
      Но все приведенное выше, еще раз повторяю, относится только к классам, наследуемым от Objective-C.
      Для любого чистого Swift класса этой проблемы нет вообще. Если вы посмотрите Лекцию 3 и Задание 2, когда мы имеем дело с чистым Swift классом CalculatorBrain, то там множество функций с одинаковыми именами и проблема не возникает.
      Так что просто будьте более внимательны с классами, наследуемыми от Objective-C.

  2. Эта проблема связа с вопросом: «Когда Swift выставляется мой класс для использования в Objective-C?»
    Исправьте! Эта проблема связана с вопросом: «Когда Swift выставляет мой класс для использования в Objective-C?»

Обсуждение закрыто.