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