Как добавить статические функции, использующие задачи, в расширениях Swift Task

Мне любопытно, сталкивался ли кто-нибудь с этим вопросом раньше. Первоначально я столкнулся с проблемами при попытке использовать расширение 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):

  1. Referencing initializer 'init(priority:operation:)' on 'Task' requires the types 'Failure' and 'any Error' be equivalent

  2. 'Cannot convert value of type '()' to closure result type 'Success'.

Почему компилятор по-разному обрабатывает расширение Task и как это решить? Я знаю, что Success и Failure — это два типа заполнителей для универсального объекта Task, но я не думаю, что они должны влиять на экземпляр Task в реализации статической функции bar.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
62
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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, не очень важен.

Спасибо за этот ответ. Я думаю, это очень хорошо отвечает на вопрос, чем расширение Task отличается от расширения Foo. Но если я попытаюсь создать расширение задачи, как вы указали, и определить свою неасинхронную панель для вызова try await Task.sleep(nanoseconds: 1000), то я получу ошибки. Изменение ограничений расширения на where Success == Never, Failure == Never дает разные ошибки, но все равно не работает. Итак, опять же, кажется, что ограничения расширения влияют на то, что делает Задача в реализации, и это кажется неправильным.

Curious Jorge 21.07.2024 05:02

@CuriousJorge, если хочешь вызвать Task.sleep сюда, напиши Task<Never, Never>.sleep(...). Я считаю, что причина этого адекватно объяснена в моем ответе.

Sweeper 21.07.2024 05:05

@CuriousJorge Аналогично, если вы хотите использовать where Success == Never, Failure == Never, вам следует написать Task<Void, Never> { ... }.

Sweeper 21.07.2024 05:07

@CuriousJorge «похоже, что ограничения расширения влияют на то, что делает Задача в реализации». Верно. Task (сам по себе, без каких-либо параметров типа) в расширении Task всегда означает Task<Success, Failure>, поэтому изменение ограничений на Success и Failure меняет значение Task. Почему это не кажется правильным?

Sweeper 21.07.2024 05:11

Я подтверждаю, что Task<Never, Never> работает с указанными вами ограничениями. Второй вопрос мне не подходит, но давайте пока проигнорируем его и сосредоточимся на последнем вопросе. Все это не имеет для меня смысла, потому что мне кажется, что когда я создаю экземпляр новой дочерней задачи, она не должна заботиться о типах успеха/возврата родительской задачи. Я даже не уверен, что имею дело именно с такой ситуацией. Я имею в виду, каков механизм привязки подписи статической функции типа Task к контексту, в котором она вызывается? Вероятно, это недостающая часть информации, мешающая мне понять.

Curious Jorge 21.07.2024 06:26

@CuriousJorge Упс, я имел в виду Task<Void, any Error> { ... } в этом комментарии. Задачи детей/родителей здесь не имеют значения. Task { ... } создает задачу верхнего уровня. «Механизм», о котором вы говорите, двоякий. Во-первых, sleep объявлен в расширении Task с ограничениями where Success == Never, Failure == Never. Мы знаем это, потому что в документации написано «доступно, когда...». Когда мы пишем Task.sleep(...) вне расширения Task, компилятор на основании этих ограничений может сделать вывод, что мы на самом деле имеем в виду Task<Never, Never>.sleep(...).

Sweeper 21.07.2024 06:32

@CuriousJorge Во-вторых, в данном конкретном случае вы звоните Task.sleep по добавочному номеру Task. Как поясняется в первой половине ответа, вместо того, чтобы делать вывод, что вы имеете в виду Task<Never, Never>.sleep(...), исходя из ограничений расширения, в котором объявлен sleep, компилятор предполагает, что вы имеете в виду Task<Success, Failure>.sleep(...). Таким образом, если Success и Failure не ограничены в вашем расширении, вы не сможете вызвать Task.sleep(...).

Sweeper 21.07.2024 06:36

@CuriousJorge Если бы sleep было объявлено без этих ограничений, то, например. Task<String, Error>.sleep(...) тоже было бы актуально. Из-за этого вызов довольно утомляет, потому что вам нужно указать два ненужных параметра типа (то, что делает sleep, не зависит ни от Success, ни от Failure). Вы бы не смогли просто сказать Task.sleep(...), потому что Task имеет два параметра типа, и компилятор не может их вывести. Но при добавлении этих ограничений становится необходимым указывать два параметра типа при вызове в расширении Task, как описано выше.

Sweeper 21.07.2024 06:44

«...компилятор может сделать вывод...» Что ж, это сюрприз. Это кажется произвольным, но полезным. У меня нет никаких знаний, подтверждающих, что это должно быть так (кроме полезности). Это вызывает столько вопросов в моей голове! Позвольте мне поиграть со всей информацией, которую вы мне предоставили. Спасибо вам за все это!

Curious Jorge 21.07.2024 06:54

Посидев немного над этим и внимательно просмотрев все вышеизложенное, теперь все стало ясно. Еще раз спасибо, Свипер!

Curious Jorge 22.07.2024 00:36

Для тех, кто зашел так далеко в комментариях, позвольте мне подвести итог обсуждения. В расширении Foo компилятор может сделать вывод, что Task, используемый в неасинхронном bar, должен иметь успех == недействительность и сбой == любую ошибку, на основе сигнатуры вызова асинхронного bar в этом Task .

Curious Jorge 22.07.2024 00:41

Но в расширении Task вывод типов заполнителей имеет меньший приоритет по сравнению с предположением, что любые задачи, используемые в функции, должны быть того же типа, для которого предназначено расширение. Сообщения об ошибках Xcodes не дают нам особой ясности в том, что происходит, но в них нет ничего нового.

Curious Jorge 22.07.2024 00:43

Таким образом, решение состоит в том, чтобы переопределить предположение компилятора, явно указав типы заполнителей (те же, которые компилятор вывел бы в контексте расширения, отличного от задачи) для Task, создаваемого в неасинхронном bar.

Curious Jorge 22.07.2024 00:50

Я бы также добавил, что я думаю, что, поскольку моя функция bar не использует тип, для которого она определена, мое расширение должно быть ограничено успехом == Никогда, Неудачей == Никогда, точно так же, как расширение, в котором определен сон. .

Curious Jorge 22.07.2024 00:53

Другие вопросы по теме