Невозможно вызвать обратный вызов, хранящийся в объекте

Я создал службу уведомлений в памяти для использования в своих базах кода Dart/Flutter. Я начал использовать шаблон Observer, но быстро обнаружил, что подписка на изменения в глубоко вложенных моделях представлений стала болезненной и запутанной.

Цель состоит в том, чтобы создать одноэлементную службу, которая сможет публиковать уведомления строго типизированным способом. Потребители могут подписаться на целевые уведомления, а также получать полезную нагрузку строго типизированным способом.

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

Unhandled exception:
type '(ExampleNotification) => void' is not a subtype of type '(PublishedNotification) => dynamic'
#0      NotificationService.publish (package:console/notification_service.dart:42:24)
#1      main (package:console/console.dart:15:23)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

Каждое уведомление расширяет базовый класс PublishedNotification.

abstract class PublishedNotification {
  String qualifier;

  PublishedNotification(this.qualifier);

  @override
  String toString() {
    return 'PublishedNotification{qualifier: $qualifier}';
  }
}
import 'package:console/published_notification.dart';

class HelloWorldNotification extends PublishedNotification {
  HelloWorldNotification() : super('Hello World Message');
}
import 'package:console/published_notification.dart';

class ExampleNotification extends PublishedNotification {
  ExampleNotification() : super('This is an example');
}

Каждое уведомление может быть опубликовано кем угодно, и уведомление получат только подписчики. Когда объект подписывается на уведомление, создается подписка, содержащая обратный вызов для вызова подписчика, а также тип уведомления, на которое нацелена подписка.

import 'dart:mirrors';

import 'package:console/published_notification.dart';

class Subscription<T extends PublishedNotification> {
  final Function(T) subscriberCallback;
  Type targetNotification = reflectClass(T).reflectedType;

  Subscription(this.subscriberCallback);
}

Подписка, публикация и отписка осуществляется через NotificationService. При вызове метода subscribe подписчик передает обратный вызов, который будет вызываться каждый раз при публикации уведомления. Он также возвращает сам Subscription, чтобы вызывающий абонент мог отказаться от подписки и выполнить очистку во время удаления.

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

import 'dart:mirrors';

import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';

class NotificationService {
  // List of subscribers grouped by notification type.
  final Map<Type, List<Subscription>> subscriptions = <Type, List<Subscription>>{};

  // Subscribe to a notification.
  // notification is a required named parameter. 
  // A secondary 'condition' parameter will be added to support conditional pushing a notification to a subscriber based on if the condition passes or not.
  Subscription<T> subscribe<T extends PublishedNotification>({required Function(T payload) notification}) {
    // Wrap notification callback into a subscriber.
    final newSubscriber = Subscription(notification);

    ClassMirror notificationClass = reflectClass(T);
    Type notificationType = notificationClass.reflectedType;

    // Store the subscriber into our subscriptions collection.
    late List<Subscription> subscribers;
    if (subscriptions[notificationType] == null) {
      subscribers = List.empty(growable: true);
      subscriptions[notificationType] = subscribers;
    }

    subscribers.add(newSubscriber);

    // Return so that it can be provided during an unsubscribe request.
    return newSubscriber;
  }

  // Publish a notification to all subscribers
  void publish<T extends PublishedNotification>(T notification) {
    Type notificationType = notification.runtimeType;
    if (subscriptions[notificationType] == null) {
      return;
    }

    List<Subscription> subscribers = subscriptions[notificationType]!;

    // Invoke the callback delegate we have stored within each subscriber.
    for (Subscription targetSubscriber in subscribers) {
      targetSubscriber.subscriberCallback(notification);
    }
  }

  // Unsubscribe the
  void unsubscribe(Subscription subscriber) {
    subscriptions[subscriber.targetNotification]?.remove(subscriber);
  }
}

У меня есть пример консольного приложения Dart, которое я создал, чтобы воспроизвести проблему, с которой я столкнулся. Я подписываюсь на два разных уведомления, публикую уведомления, а затем отказываюсь от подписки. Когда HelloWorldNotification публикуется, мой обратный вызов вызывается без каких-либо проблем. Когда ExampleNotification публикуется, я получаю исключение, которое показано выше.

import 'package:console/example_notification.dart';
import 'package:console/hello_world_notification.dart';
import 'package:console/notification_service.dart';
import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';

void main() {
  var notificationService = NotificationService();
  Subscription helloWorldSubcription =
      notificationService.subscribe<HelloWorldNotification>(notification: handleHelloWorldNotification);
  Subscription exampleSubscription =
      notificationService.subscribe<ExampleNotification>(notification: handleExampleNotification);

  notificationService.publish(HelloWorldNotification());
  notificationService.publish(ExampleNotification());

  notificationService.unsubscribe(helloWorldSubcription);
  notificationService.unsubscribe(exampleSubscription);

  // These should not hit the handlers below.
  notificationService.publish(HelloWorldNotification());
  notificationService.publish(ExampleNotification());
}

void handleHelloWorldNotification(PublishedNotification payload) {
  print(payload.qualifier);
}

void handleExampleNotification(ExampleNotification payload) {
  print(payload.qualifier);
}

Я не понимаю, почему первое уведомление работает нормально, а второе нет. Они оба одинаковы с точки зрения реализации, и обратные вызовы ничем не отличаются. Я предполагаю, что это проблема с тем, как я делаю выводы об дженериках внутри NotificationService, но я не уверен на 100%, почему это так.

Я могу выполнить приведение в цикле for для каждого Subscription, и проблема исчезнет, ​​и все будет работать как положено.

  // Publish a notification to all subscribers
  void publish<T extends PublishedNotification>(T notification) {
    Type notificationType = notification.runtimeType;
    if (subscriptions[notificationType] == null) {
      return;
    }

    List<Subscription> subscribers = subscriptions[notificationType]!;

    // Invoke the callback delegate we have stored within each subscriber.
    for (Subscription targetSubscriber in subscribers) {
      var s = targetSubscriber as Subscription<T>; // Cast here
      s.subscriberCallback(notification);
    }
  }

Есть ли что-то явно неправильное в моей реализации метода publish и в том, как я храню обратные вызовы подписки? У меня есть исправление, но я хочу понять причину, по которой первая публикация работает, а вторая нет, если я не принудительно приведу актеров. Будет ли в конечном итоге требоваться приведение типов или есть способ немного провести рефакторинг, чтобы приведение типов не требовалось?

Спасибо!

Редактировать

Хорошо, похоже, что dart:mirrors не поддерживается во Flutter. Мне пришлось искать другой способ сбора информации о типах, и я обнаружил, что мне на самом деле не нужно использовать reflactClass. Когда я обновил свой NotificationService, чтобы исключить использование dart:mirrors, исключения перестали возникать.

import 'dart:mirrors';

import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';

class NotificationService {
  // List of subscribers grouped by notification type.
  final Map<Type, List<Subscription>> subscriptions = <Type, List<Subscription>>{};

  // Subscribe to a notification.
  @override
  Subscription<T> subscribe<T extends PublishedNotification>({required Function(T payload) notification}) {
    // Wrap notification callback into a subscriber.
    final newSubscriber = Subscription(notification);

    // Store the subscriber into our subscriptions collection.
    late List<Subscription> subscribers;
    if (subscriptions[T] == null) {
      subscribers = List.empty(growable: true);
      subscriptions[T] = subscribers;
    }

    subscribers.add(newSubscriber);

    // Return so that it can be provided during an unsubscribe request.
    return newSubscriber;
  }

  // Publish a notification to all subscribers
  @override
  void publish<T extends PublishedNotification>(T notification) {
    if (subscriptions[T] == null) {
      return;
    }

    List<Subscription> subscribers = subscriptions[T]!;

    // Invoke the callback delegate we have stored within each subscriber.
    for (Subscription targetSubscriber in subscribers) {
      targetSubscriber.notify(notification);
    }
  }

  // Unsubscribe the
  @override
  void unsubscribe(Subscription subscriber) {
    subscriptions[subscriber.targetNotification]?.remove(subscriber);
  }
}

Это изменение также коснулось моего класса Subscription.

class Subscription<T extends PublishedNotification> {
  final Function(T) _subscriberCallback;
  Type targetNotification = T;

  Subscription(this._subscriberCallback);

  void notify(T notification) {
    _subscriberCallback(notification);
  }
}

Актерский состав мне больше не нужен, и кажется, все работает так, как ожидалось. Мне все еще интересно понять, почему reflectClass.reflectedType вызвало проблемы, но просто перейти T прямо ко всему - нет.

Я не получаю исключения при попытке запустить ваш код.

mmcdon20 07.04.2024 23:06

Вы проводите его с актерами, которые я показываю внизу, или без них? У меня не возникает исключения, когда я бегу с актерским составом, но я обходлюсь без него.

Johnathon Sullinger 07.04.2024 23:24

Не вижу исключения с приведением as Subscription<T> или без него.

mmcdon20 07.04.2024 23:28

Хорошо, я удалил приведение, и теперь оно тоже компилируется. Я провел некоторый рефакторинг (и обновил свой пост, чтобы отразить это). Если я вернусь к использованию Type targetClass = reflectClass(T).reflectedType вместо нового способа доступа к типу, это выдаст исключение. Я верну свой обновленный пост к оригиналу, а затем опубликую ответ, показывающий устранение использования dart:mirrors, решенное для него. Однако мне хотелось бы знать, почему dart:mirrors выдает исключение.

Johnathon Sullinger 07.04.2024 23:30

Хорошо @mmcdon20 — я вернул свое сообщение к оригиналу, поэтому теперь случаются исключения. Я также отредактировал его, чтобы показать внесенные мной изменения (передавая везде T вместо reflectClass(T).reflectedType и как это решило проблему.

Johnathon Sullinger 07.04.2024 23:42

Глядя на ваш код, видно, что изменение между T и reflectClass(T).reflectedType не устранило проблему, вместо этого вы ввели новую функцию notify в свой класс Subscription, и это, похоже, изменение, которое остановило возникновение исключения.

mmcdon20 08.04.2024 00:11

Другими словами, targetSubscriber.subscriberCallback(notification); вызывает исключение, а targetSubscriber.notify(notification); — нет. Я не совсем понимаю, почему на данный момент.

mmcdon20 08.04.2024 00:16

Кроме того, это несвязанная проблема: вы неправильно используете late в функции подписки. Это вызовет исключение, если вы попытаетесь подписаться на один и тот же тип дважды. Вместо этого вам, вероятно, нужен тип, допускающий значение NULL.

mmcdon20 08.04.2024 00:21

Спасибо что подметил это. Я вернулся, провел повторное тестирование и теперь тоже это вижу. Я предполагаю, что это связано с тем, что когда вызывается обратный вызов, он по какой-то причине не знает, что такое T (несмотря на то, что он доступен в методе publish), тогда как когда я вызываю обратный вызов из метода notify, T известен. Думаю, я бы в какой-то степени понял это, если бы не тот факт, что первая публикация может это понять, а вторая — нет. Возможно, T захватывается как HelloWorldNotification внутри этого контекста во время выполнения и не освобождается при поступлении второго вызова?

Johnathon Sullinger 08.04.2024 00:21

Ну, в первом примере обратный вызов принимает PublishedNotification в качестве аргумента, а во втором примере — более конкретный ExampleNotification. Если вы обновите первый обратный вызов, чтобы принять HelloWorldNotification, то ни то, ни другое не сработает.

mmcdon20 08.04.2024 00:25

Ах хорошо. Я совершенно не заметил, что у моих обратных вызовов была другая подпись. Спасибо что подметил это. Я также заменил использование late нулевым оператором присваивания - List<Subscription> subscribers = subscriptions[notificationType] ??= List.empty(growable: true); Так что, в конце концов, это связано с тем фактом, что T неизвестен, когда обратный вызов вызывается из метода publish. Даже если я изменю код примера кода, включив <T>, например service.publish<ExampleNotification>(ExampleNotification());, он все равно не сможет определить T в методе publish.

Johnathon Sullinger 08.04.2024 00:32

Думаю, я разобрался в проблеме. Я опубликую ответ ниже.

mmcdon20 08.04.2024 00:55
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
12
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Рассмотрим следующий код:

class PublishedNotification {}
class Subscription<T extends PublishedNotification> {  }
class GenericClass<T> {}

void main() {
  print(GenericClass().runtimeType); // GenericClass<dynamic>
  print(Subscription().runtimeType); // Subscription<PublishedNotification>
}

Как вы можете видеть выше, поскольку вы ограничили универсальный тип предложением T extends PublishedNotification, когда вы используете Subscription в качестве необработанного типа, он теперь эквивалентен Subscription<PublishedNotification>, а не Subscription<dynamic>, как вы могли бы ожидать.

Имея это в виду, следующий код

for (Subscription targetSubscriber in subscribers) {
  targetSubscriber.subscriberCallback(notification);
}

действительно,

for (Subscription<PublishedNotification> targetSubscriber in subscribers) {
  targetSubscriber.subscriberCallback(notification);
}

И если вы наведете указатель мыши на targetSubscriber в редакторе, он сообщит вам, что это тип Subscription<PublishedNotification>.

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

void handleHelloWorldNotification(PublishedNotification payload) {
  print(payload.qualifier);
}

void handleExampleNotification(ExampleNotification payload) {
  print(payload.qualifier);
}

С первым обратным вызовом проблем нет, поскольку типы совпадают с PublishedNotification, но со вторым обратным вызовом возникает проблема. Subscription<PublishedNotification> должен иметь возможность использовать обратный вызов для любого PublishedNotification, но фактический обратный вызов ограничен только принятием ExampleNotification, поэтому и возникает исключение.

Ах, это имеет смысл, если увидеть это таким образом. Итак, это сводится к проблеме дисперсии — с моим использованием дженериков. Это объясняет, почему вызов notify в классе Subscription решает эту проблему, поскольку T явно является дочерним классом в этом контексте. Спасибо!

Johnathon Sullinger 08.04.2024 01:28

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