Мне любопытно, сталкивался ли кто-нибудь с этим вопросом раньше. Первоначально я столкнулся с проблемами при попытке использовать расширение Task
, которое вызывает Task.sleep
из статической функции, не являющейся асинхронной, но дальнейшее исследование привело меня к более простому вопросу.
Это действительно Swift:
struct Foo {}
extension Foo {
static func bar() async throws {
}
static func bar() {
Task {
try await bar()
}
}
}
Но нет следующего:
extension Task {
static func bar() async throws {
}
static func bar() {
Task {
try await bar()
}
}
}
Это дает мне две ошибки (в Xcode 15.4):
Referencing initializer 'init(priority:operation:)' on 'Task' requires the types 'Failure' and 'any Error' be equivalent
'Cannot convert value of type '()' to closure result type 'Success'
.
Почему компилятор по-разному обрабатывает расширение Task
и как это решить? Я знаю, что Success
и Failure
— это два типа заполнителей для универсального объекта Task, но я не думаю, что они должны влиять на экземпляр Task в реализации статической функции bar
.
Когда вы пишете имя расширенного типа в расширении без каких-либо параметров типа, предполагается, что вы имеете в виду использовать уже объявленные параметры типа. В конце концов, именно это и происходит в собственном объявлении типа:
struct Foo<T> {
func foo() {
let x = Foo() // "Foo()" means "Foo<T>()"
}
var bar: Foo { // this means "var bar: Foo<T>"
Foo()
}
}
В расширении вы находитесь «как будто» в объявлении типа.
extension Set {
func foo() {
let x = Set() // "Set()" means "Set<Element>()"
}
var bar: Set { // this means "var bar: Set<Element>"
[]
}
}
Итак, в вашем случае предполагается, что вы имеете в виду Task<Success, Failure> { ... }
, т. е. вы создаете задачу, которая возвращает любой тип, который хочет вызывающий объект, и может выдавать любой тип ошибки, который хочет выдать вызывающий объект. Это явно не то, что вы хотите.
Вам следует добавить ограничения к Success
и Failure
:
extension Task where Success == Void, Failure == any Error {
static func bar() async throws {
}
static func bar() {
Task {
try await bar()
// as discussed below, Task.sleep requires that Success == Never, Failure == Never
// but writing 'Task' on its own here would mean Task<Void, any Error>
try await Task<Never, Never>.sleep(...)
}
}
}
Также возможно напрямую записать параметры типа, не добавляя ограничений к Success
и Failure
.
Task<Void, any Error> {
try await bar()
try await Task<Never, Never>.sleep(...)
}
Однако это делает сайт вызова более громоздким — нельзя просто написать:
Task.bar()
потому что Task
нужны два параметра типа, и компилятор не может их вывести.
Обратите внимание, что другие статические методы Task
также ограничивают типы Success
и Failure
. например в документации для сна написано:
Доступно, когда
Success
естьNever
иFailure
естьNever
.
Это сделано для того, чтобы помочь компилятору определить параметры типа, чтобы вызывающая сторона могла просто написать Task.sleep(...)
вместо Task<Never, Never>.sleep(...)
. Точный тип, которым ограничены Success
и Failure
, не очень важен.
@CuriousJorge, если хочешь вызвать Task.sleep
сюда, напиши Task<Never, Never>.sleep(...)
. Я считаю, что причина этого адекватно объяснена в моем ответе.
@CuriousJorge Аналогично, если вы хотите использовать where Success == Never, Failure == Never
, вам следует написать Task<Void, Never> { ... }
.
@CuriousJorge «похоже, что ограничения расширения влияют на то, что делает Задача в реализации». Верно. Task
(сам по себе, без каких-либо параметров типа) в расширении Task
всегда означает Task<Success, Failure>
, поэтому изменение ограничений на Success
и Failure
меняет значение Task
. Почему это не кажется правильным?
Я подтверждаю, что Task<Never, Never> работает с указанными вами ограничениями. Второй вопрос мне не подходит, но давайте пока проигнорируем его и сосредоточимся на последнем вопросе. Все это не имеет для меня смысла, потому что мне кажется, что когда я создаю экземпляр новой дочерней задачи, она не должна заботиться о типах успеха/возврата родительской задачи. Я даже не уверен, что имею дело именно с такой ситуацией. Я имею в виду, каков механизм привязки подписи статической функции типа Task к контексту, в котором она вызывается? Вероятно, это недостающая часть информации, мешающая мне понять.
@CuriousJorge Упс, я имел в виду Task<Void, any Error> { ... }
в этом комментарии. Задачи детей/родителей здесь не имеют значения. Task { ... }
создает задачу верхнего уровня. «Механизм», о котором вы говорите, двоякий. Во-первых, sleep
объявлен в расширении Task
с ограничениями where Success == Never, Failure == Never
. Мы знаем это, потому что в документации написано «доступно, когда...». Когда мы пишем Task.sleep(...)
вне расширения Task
, компилятор на основании этих ограничений может сделать вывод, что мы на самом деле имеем в виду Task<Never, Never>.sleep(...)
.
@CuriousJorge Во-вторых, в данном конкретном случае вы звоните Task.sleep
по добавочному номеру Task
. Как поясняется в первой половине ответа, вместо того, чтобы делать вывод, что вы имеете в виду Task<Never, Never>.sleep(...)
, исходя из ограничений расширения, в котором объявлен sleep
, компилятор предполагает, что вы имеете в виду Task<Success, Failure>.sleep(...)
. Таким образом, если Success
и Failure
не ограничены в вашем расширении, вы не сможете вызвать Task.sleep(...)
.
@CuriousJorge Если бы sleep
было объявлено без этих ограничений, то, например. Task<String, Error>.sleep(...)
тоже было бы актуально. Из-за этого вызов довольно утомляет, потому что вам нужно указать два ненужных параметра типа (то, что делает sleep
, не зависит ни от Success
, ни от Failure
). Вы бы не смогли просто сказать Task.sleep(...)
, потому что Task
имеет два параметра типа, и компилятор не может их вывести. Но при добавлении этих ограничений становится необходимым указывать два параметра типа при вызове в расширении Task
, как описано выше.
«...компилятор может сделать вывод...» Что ж, это сюрприз. Это кажется произвольным, но полезным. У меня нет никаких знаний, подтверждающих, что это должно быть так (кроме полезности). Это вызывает столько вопросов в моей голове! Позвольте мне поиграть со всей информацией, которую вы мне предоставили. Спасибо вам за все это!
Посидев немного над этим и внимательно просмотрев все вышеизложенное, теперь все стало ясно. Еще раз спасибо, Свипер!
Для тех, кто зашел так далеко в комментариях, позвольте мне подвести итог обсуждения. В расширении Foo
компилятор может сделать вывод, что Task
, используемый в неасинхронном bar
, должен иметь успех == недействительность и сбой == любую ошибку, на основе сигнатуры вызова асинхронного bar
в этом Task
.
Но в расширении Task
вывод типов заполнителей имеет меньший приоритет по сравнению с предположением, что любые задачи, используемые в функции, должны быть того же типа, для которого предназначено расширение. Сообщения об ошибках Xcodes не дают нам особой ясности в том, что происходит, но в них нет ничего нового.
Таким образом, решение состоит в том, чтобы переопределить предположение компилятора, явно указав типы заполнителей (те же, которые компилятор вывел бы в контексте расширения, отличного от задачи) для Task
, создаваемого в неасинхронном bar
.
Я бы также добавил, что я думаю, что, поскольку моя функция bar
не использует тип, для которого она определена, мое расширение должно быть ограничено успехом == Никогда, Неудачей == Никогда, точно так же, как расширение, в котором определен сон. .
Спасибо за этот ответ. Я думаю, это очень хорошо отвечает на вопрос, чем расширение Task отличается от расширения Foo. Но если я попытаюсь создать расширение задачи, как вы указали, и определить свою неасинхронную панель для вызова
try await Task.sleep(nanoseconds: 1000)
, то я получу ошибки. Изменение ограничений расширения наwhere Success == Never, Failure == Never
дает разные ошибки, но все равно не работает. Итак, опять же, кажется, что ограничения расширения влияют на то, что делает Задача в реализации, и это кажется неправильным.