Как создать обратный вызов таймера, который может редактировать свой объект состояния?

У меня есть System.Threading.Timer, который получает некоторые данные каждую секунду, и я хочу, чтобы его обратный вызов помещал полученные данные в объект, который должен быть возвращен клиентскому коду. Я попытался реализовать это, используя механизм ссылочных типов С#. Вот текущий обратный вызов:

private void Receive(object? obj) {
  var data = GetData();

  obj = (object)data; // Does nothing
}

А вот метод запуска таймера (timer — это поле):

public void Start(Image image) {
  timer = new Timer(
    Receive,
    image,
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(1)
  );
}

Ожидаемое поведение: каждую секунду данные (которые являются данными изображения) принимаются и помещаются в изображение, ссылка на которое указана в параметре image.

Фактическое поведение: когда я определил переменную типа Image, передал ее в метод Start и проверил ее значение (с помощью отладчика Visual Studio) через 5 секунд, это было пустое Bitmap, которое я присвоил ей в самом начале.

Я также проверил, что данные (содержащие непустое изображение) отправляются правильно.

Может ли кто-нибудь сказать мне, в чем проблема, или, в качестве альтернативы, предложить другой способ изменить объект «состояние» в обратном вызове таймера?

Стоит ли изучать 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
0
127
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В методе Recieve вы получаете объект в качестве входной ссылки. Затем вы пытаетесь установить значение obj. Этот последний шаг не может работать, так как вы работаете со ссылкой. Единственный способ, которым это может работать, - это если входная ссылка указана как ref, что

private void Receive(ref object? obj) {}

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

Это не работает из-за делегата TimerCallback. Он определяет простой параметр object (не ссылку).

SNBS 27.12.2022 18:03
Ответ принят как подходящий

obj = (object)data; устанавливает данные только локально в методе Receive. Вы можете исправить это, создав объект-оболочку

public class ImageData
{
    public Image Image { get; set; }
}

Затем создайте его статический экземпляр, доступ к которому можно получить оттуда, где вам это нужно.

public static readonly ImageData Data = new ImageData();

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

Тогда начните так

public void Start() {
  timer = new Timer(
    Receive,
    Data,
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(1)
  );
}

И получить

private void Receive(object? obj) {
  var data = (ImageData)obj;  
  data.Image = GetData();
}

Теперь вы можете получить доступ к новым изображениям через статическое свойство Data.Image.


В качестве альтернативы вы можете «использовать» изображение (то есть отображать или что-то еще, что вы с ним делаете) непосредственно в обратном вызове Receive и игнорировать параметр obj. Однако мне не хватает контекстной информации (где находятся различные фрагменты кода, в каком потоке они выполняются и т. д.).


Обновлять

Изображение будет null в ImageData первым, пока обратный вызов Receiver не будет вызван в первый раз.

Если вам нужно изображение в самом начале, вы можете сделать несколько вещей.

  1. Запустите таймер с аргументом first time равным 0 (это время ожидания до первого срабатывания).

  2. Вызов обратного вызова прямо в Start: Receive(Data).

  3. Назначьте изображение с помощью Data.Image = GetData();.

  4. Инициализируйте изображение в ImageData фиктивным изображением (может быть, каким-то изображением ожидания).


Обновление 2

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

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

В библиотеке мы объявляем аргументы события как

public class UpdateImageEventArgs : EventArgs
{
    public UpdateImageEventArgs(Image image)
    {
        Image = image;
    }

    public Image Image { get; }
}

Возможная реализация библиотеки:

public class Library : IDisposable
{
    public event EventHandler<UpdateImageEventArgs>? ImageUpdated;

    private System.Threading.Timer? _timer;

    public void Start()
    {
        _timer = new System.Threading.Timer(Receive, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }

    private void Receive(object? obj)
    {
        var image = GetImage();
        ImageUpdated?.Invoke(this, new UpdateImageEventArgs(image));
    }

    private static Image GetImage()
    {
        // TODO: Add your implementation
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        if (_timer != null) {
            _timer.Dispose();
            _timer = null;
        }
    }
}

Код потребителя:

public static void Test()
{
    var library = new Library();
    library.ImageUpdated += Library_ImageUpdated;
    library.Start();
}

private static void Library_ImageUpdated(object? sender, UpdateImageEventArgs e)
{
    DoSomethingWith(e.Image);
}

Если вы хотите остановить таймер, вы можете сделать это, позвонив library.Dispose();. Это также гарантирует правильное размещение ресурсов таймера.

Вместо ImageData вы можете использовать встроенный StrongBox<T>.

Theodor Zoulias 27.12.2022 18:19

@TheodorZoulias Я бы не стал использовать тип из System.Runtime.CompilerServices в коде общего назначения. У него даже есть описание: «Этот тип предоставляется для использования динамическими методами, которые генерирует внутренний компонент компилятора выражений API дерева выражений».

Evk 27.12.2022 18:23

Упоминание статической переменной кажется немного запутанным. Было бы более понятно просто заменить Image на ImageData в обратном вызове таймера public void Start(Image image).

Evk 27.12.2022 18:33

Нет, все в порядке. Вы наверняка используете .GetAwaiter().GetResult() для задач, которые находятся в том же пространстве имен. StrongBox<T> — это класс с одним изменяемым полем и без логики. Я не вижу ничего плохого в использовании.

Theodor Zoulias 27.12.2022 18:34

Не работает. Стандартный конструктор ImageData устанавливает Image в null, и я вижу это null, когда читаю его.

SNBS 27.12.2022 20:23

@SNBS, похоже, вы хотите установить изображение в приемнике с помощью GetData() и вернуть его обратно через параметр obj, который является Imagedata. Таким образом, Image сначала будет нулевым в ImageData, пока обратный вызов Receiver не будет вызван в первый раз. С этого момента он всегда будет содержать последнее назначенное изображение.

Olivier Jacot-Descombes 28.12.2022 13:29

Если вы говорите, что я должен убедиться, что Receive вызывается хотя бы один раз — это правда, и после этого я читаю изображение из ImageData. Как видите, мой таймер вызывает свой обратный вызов каждую секунду, и мой основной поток спит в течение 3 секунд перед чтением. И я поставил Console.WriteLine("Elapsed") в обратный вызов. Трижды было написано «Прошло».

SNBS 28.12.2022 16:17

Спасибо за обновление вашего ответа, но я не могу «использовать» изображение в методе Receive, потому что я пишу библиотеку классов (теперь это консольное приложение, чтобы упростить тесты, но выпуск будет библиотекой). Изображение должно быть возвращено в клиентский код.

SNBS 28.12.2022 16:20

@SNBS вопрос ничего не говорит о том, что вы пишете библиотеку классов и вам нужно вернуть изображение в клиентский код. Вопрос заключается в том, как сохранить изображение в состоянии Timer. Что бессмысленно, потому что состояние недоступно вне обработчика событий, но это то, о чем вы просили. И ответ Оливье по существу. Если вы хотите узнать что-то еще, я бы предложил задать новый вопрос, потому что на этот вопрос был дан ответ.

Theodor Zoulias 28.12.2022 17:21

@TheodorZoulias Нет, на этот вопрос нет ответа. В одном из своих комментариев к этому ответу я сказал, что свойство Image в моем ImageData остается null (которое было присвоено ему стандартным конструктором ImageData). Я обновил свой вопрос, упомянув библиотеку классов и клиентский код.

SNBS 28.12.2022 17:46

Дело в том, что мы не знаем, кто чем занимается. Где обратный звонок? В библиотеке или в коде потребителя? Или, другими словами: получает ли библиотека изображение и передает его потребителю, или потребитель получает изображение (где библиотека предоставляет только таймер?

Olivier Jacot-Descombes 28.12.2022 17:50

Использование событий помогло мне, спасибо! (Странно, что я сам их не запомнил.) И еще раз спасибо за такой подробный ответ. Наградил вас наградой.

SNBS 28.12.2022 20:37

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