Swift: переопределение самообслуживания допускается, но вызывает ошибку времени выполнения. Зачем?

Я только начал изучать Swift (v. 2.x), потому что мне любопытно, как появляются новые функции, особенно протоколы с Self-требованиями.

Следующий пример будет компилироваться просто отлично, но вызывает произвольные эффекты выполнения:

// The protocol with Self requirement
protocol Narcissistic {
    func getFriend() -> Self
}


// Base class that adopts the protocol
class Mario : Narcissistic  {
    func getFriend() -> Self {
        print("Mario.getFriend()")
        return self;
    }
}


// Intermediate class that eliminates the
// Self requirement by specifying an explicit type
// (Why does the compiler allow this?)
class SuperMario : Mario {
    override func getFriend() -> SuperMario {
        print("SuperMario.getFriend()")
        return SuperMario();
    }
}

// Most specific class that defines a field whose
// (polymorphic) access will cause the world to explode
class FireFlowerMario : SuperMario {
    let fireballCount = 42

    func throwFireballs() {
        print("Throwing " + String(fireballCount) + " fireballs!")
    }
}


// Global generic function restricted to the protocol
func queryFriend<T : Narcissistic>(narcissistic: T) -> T {
    return narcissistic.getFriend()
}


// Sample client code

// Instantiate the most specific class
let m = FireFlowerMario()

// The call to the generic function is verified to return
// the same type that went in -- 'FireFlowerMario' in this case.
// But in reality, the method returns a 'SuperMario' and the
// call to 'throwFireballs' will cause arbitrary
// things to happen at runtime.
queryFriend(m).throwFireballs()

Здесь вы можете увидеть пример в действии в IBM Swift Sandbox. В моем браузере вывод выглядит следующим образом:

SuperMario.getFriend()
Throwing 32 fireballs!

(вместо 42! Или, скорее, "вместо исключения времени выполнения", поскольку этот метод даже не определен для объекта, на который он вызван.)

Является ли это доказательством того, что Swift в настоящее время не безопасен для типов?

EDIT # 1:

Непредсказуемое поведение, подобное этому, должно быть неприемлемым. Истинный вопрос: какой точный смысл имеет ключевое слово Self (первая буква). Я не мог найти ничего в Интернете, но есть, по крайней мере, эти две возможности:

  • Self - просто синтаксический ярлык для полного имени класса, в котором он появляется, и он может быть заменен последним без какого-либо изменения смысла. Но тогда он не может иметь то же значение, что и при определении протокола.

  • Self - это своего рода общий/связанный тип (как в протоколах, так и в классах), который повторно создается в процессе получения/принятия классов. Если это так, компилятор должен был отказаться от переопределения getFriend в SuperMario.

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

Ответ 1

Да, похоже, есть противоречие. Ключевое слово Self, когда оно используется как возвращаемый тип, по-видимому, означает "я как пример Я". Например, учитывая этот протокол

protocol ReturnsReceived {

    /// Returns other.
    func doReturn(other: Self) -> Self
}

мы не можем реализовать его следующим образом

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Self {
        return other    // Error
    }
}

потому что мы получаем ошибку компилятора ( "Невозможно преобразовать возвращаемое выражение типа" Return ", чтобы вернуть тип" Self "), который исчезает, если мы нарушаем doReturn() и возвращаем self вместо другого. И мы не можем писать

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {    // Error
        return other
    }
}

потому что это разрешено только в конечном классе, даже если Swift поддерживает ковариантные типы возврата. (Следующие фактически компилируются.)

final class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {
        return other
    }
}

С другой стороны, как вы отметили, подкласс Return может "переопределить" требование "Я" и весело выполнять контракт ReturnsReceived, как если бы Self были простым заполнителем для имени соответствующего класса.

class SubReturn: Return {

    override func doReturn(other: Return) -> SubReturn {
        // Of course this crashes if other is not a
        // SubReturn instance, but let ignore this
        // problem for now.
        return other as! SubReturn
    }
}

Я мог ошибаться, но я думаю, что:

  • если Self как возвращаемый тип действительно означает "я" как пример Self ', компилятор не должен принимать такое требование Self переопределяя, поскольку он позволяет возвращать экземпляры, которые не являются самими собой; в противном случае,

  • если Self как возвращаемый тип должен быть просто заполнителем без каких-либо дополнительных последствий, тогда в нашем примере компилятор должен уже разрешить переопределять требование Self в классе Return.

Таким образом, и здесь любой выбор относительно точной семантики "Я" не обязан менять вещи, ваш код иллюстрирует один из тех случаев, когда компилятор может быть легко обманут, и лучшее, что он может сделать, это генерировать код для отсрочки проверок на время выполнения. В этом случае проверки, которые должны быть делегированы во время выполнения, связаны с кастингом, и, на мой взгляд, один интересный аспект, показанный вашими примерами, заключается в том, что в определенном месте Свифт, кажется, не делегирует что-либо, следовательно, неизбежный крушение более драматичен чем это должно быть.

Swift может проверять трансляции во время выполнения. Рассмотрим следующий код.

let sm = SuperMario()
let ffm = sm as! FireFlowerMario
ffm.throwFireballs()

Здесь мы создаем SuperMario и понижаем его до FireFlowerMario. Эти два класса не связаны друг с другом, и мы гарантируем компилятору (как!), Что мы знаем, что делаем, поэтому компилятор оставляет его как есть и компилирует вторую и третью строки без заминки. Однако программа не работает во время выполнения, жалуясь, что она

Could not cast value of type
'SomeModule.SuperMario' (0x...) to
'SomeModule.FireFlowerMario' (0x...).

при попытке трансляции во второй строке. Это не является неправильным или удивительным поведением. Java, например, будет делать то же самое: скомпилировать код и сбой во время выполнения с помощью ClassCastException. Важно то, что приложение надежно падает во время выполнения.

Ваш код - более сложный способ обмануть компилятор, но он сводится к одной и той же проблеме: есть SuperMario вместо FireFlowerMario. Разница в том, что в вашем случае мы не получаем мягкое сообщение "не может отбрасывать", но в реальном проекте Xcode возникает крутая и потрясающая ошибка при вызове throwFireballs().

В той же ситуации Java терпит неудачу (во время выполнения) с той же ошибкой, которую мы видели выше (ClassCastException), что означает, что он пытается выполнить бросок (до FireFlowerMario) до вызова throwFireballs() объекта, возвращаемого queryFriend (). Наличие явной команды checkcast в байт-коде легко подтверждает это.

Скорее наоборот, насколько я могу видеть на данный момент, не пытаюсь выполнить кастинг перед вызовом (в скомпилированном коде не вызывается процедура кастинга), поэтому единственная возможная ошибка - это ужасная ошибка. Если вместо этого ваш код создавал сообщение об ошибке "не мог", или что-то столь же милое, как я, я был бы полностью удовлетворен поведением языка.

Ответ 2

Проблема здесь кажется нарушением в контракте:

Вы определяете getFriend(), чтобы вернуть экземпляр приемника (Self). Проблема здесь в том, что SuperMario не возвращает self, но возвращает новый экземпляр типа SuperMario.

Теперь, когда FireFlowerMario наследует этот метод, контракт говорит, что метод должен возвращать FireFlowerMario, но вместо этого унаследованный метод возвращает SuperMario! Этот экземпляр затем обрабатывается так, как если бы это был FireFlowerMario, а именно: Swift пытается получить доступ к переменной экземпляра fireballCount, которая не существует на SuperMario, и вместо этого вы получаете мусор.

Вы можете исправить это следующим образом:

class SuperMario : Mario {
    required override init() {
        super.init()
    }

    override func getFriend() -> SuperMario {
        print("SuperMario.getFriend()")

        // Dynamically create new instance of the same type as the receiver.
        let myClass = self.dynamicType
        return myClass.init()
    }
}

Почему компилятор разрешает это? Наверное, это трудно поймать что-то вроде этого. Для SuperMario контракт остается в силе: метод getFriend возвращает экземпляр того же класса. Контракт прерывается при создании подкласса FireFlowerMario: должен ли компилятор заметить, что суперкласс может нарушить контракт? Это было бы дорогостоящей проверкой и, вероятно, более подходящей для статического анализатора, IMHO (Также, что произойдет, если компилятор не имеет доступа к источнику SuperMario? Что произойдет, если этот класс из библиотеки?)

Таким образом, на самом деле SuperMario обязан гарантировать, что договор остается действительным при подклассов.