Предотвращение доступа к статическому свойству до его инициализации асинхронной функцией - PullRequest
2 голосов
/ 19 июня 2019

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

Мой статический класс с асинхронной функцией:

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Xml.Linq;

using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.AddressableAssets;

namespace Drok.Localization
{
    public static class Localization
    {
        /// <summary>
        /// The currently available languages.
        /// </summary>
        public static List<string> Available { get; private set; } = new List<string>();
        /// <summary>
        /// The currently selected language.
        /// </summary>
        public static string Current { get; private set; } = null;

        public static async Task Initialize()
        {
            await LoadMetaData();
        }

        private static async Task LoadMetaData()
        {
            AsyncOperationHandle<TextAsset> handle = Addressables.LoadAssetAsync<TextAsset>("Localization/meta.xml");
            TextAsset metaDataFile = await handle.Task;
            XDocument metaXMLData = XDocument.Parse(metaDataFile.text);
            IEnumerable<XElement> elements = metaXMLData.Element("LangMeta").Elements();
            foreach (XElement e in elements)
            {
                string lang = e.Attribute("lang").Value;
                int id = Int32.Parse(e.Attribute("id").Value);
                Debug.LogFormat("Language {0} is availible with id {1}.", lang, id);
                Available.Add(lang);
            }
        }

        public static void LoadLanguage(string lang)
        {
            Current = lang;
            throw new NotImplementedException();
        }

        public static string GetString(string key)
        {
            return key;
        }
    }
}

Класс, который инициализирует его и получает доступ к списку:

using Drok.Localization;

using UnityEngine;

namespace Spellbound.Menu
{
    public class LanguageMenu : MonoBehaviour
    {
        private async void Awake()
        {
            await Localization.Initialize();
        }

        private void Start()
        {
            Debug.Log(Localization.Available.Count);
        }

        private void Update()
        {

        }
    }
}

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

Ответы [ 5 ]

3 голосов
/ 19 июня 2019

A Task<T> представляет некоторое значение (типа T), которое будет определено в будущем. Если вы сделаете ваше свойство такого типа, то оно заставит всех вызывающих к await загрузить его:

public static class Localization
{
  public static Task<List<string>> Available { get; private set; }

  static Localization() => Available = LoadMetaDataAsync();

  private static async Task<List<string>> LoadMetaDataAsync()
  {
    var results = new List<string>();
    ...
      results.Add(lang);
    return results;
  }
}

Использование:

private async Task StartAsync()
{
  var languages = await Localization.Available;
  Debug.Log(languages.Available.Count);
}
2 голосов
/ 19 июня 2019

Одной из возможностей может быть добавление некоторой логики для ожидания загрузки метаданных при возврате списка из средства доступа get.

Один из способов сделать это состоит в том, чтобы в поле bool было установлено значение true, когда список готов, а затем мы либо возвращаем личную поддержку List<string> или null, в зависимости от значения наше bool поле:

public static class Localization
{
    private static bool metadataLoaded = false;
    private static List<string> available = new List<string>();

    // The 'Available' property returns null until the private list is ready
    public static List<string> Available => metadataLoaded ? available : null;

    private static async Task LoadMetaData()
    {
        // Add items to private 'available' list here

        // When the list is ready, set our field to 'true'
        metadataLoaded = true;
    }
}
2 голосов
/ 19 июня 2019

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

Однако вы можете сохранить задачу и ждать ее вStart метод, чтобы убедиться, что он завершен.Ожидание этого дважды ничего не вредит.

public class LanguageMenu : MonoBehaviour
{
    private Task _task;

    private async void Awake()
    {
        _task = Localization.Initialize();
        await _task;
    }

    private async void Start()
    {
        await _task;
        Debug.Log(Localization.Available.Count);
    }

    private void Update()
    {

    }
}
1 голос
/ 19 июня 2019

Расширение комментария Руфуса:

Объявить свойство bool, инициализированное false.А в получателе вашего списка возвращайте список только в том случае, если указанное свойство bool равно true, и, возможно, верните null (это зависит от ваших требований), если false.

public static bool IsAvailable { get; set; } = false;

private static List<string> _available;
public static List<string> Available
{
    get
    {
        if (IsAvailable)
            return _available;
        else
            return null;
    }
    set { _available = value; }
}

Наконецв вашей функции async, когда работа завершена, установите для свойства выше значение true.

0 голосов
/ 19 июня 2019

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

Обычно всегда есть одна большая альтернатива использованию async для сообщений Unity: система событий, например,

public static class Localization
{
    public static event Action OnLocalizationReady;

    public static async void Initialize()
    {
        await LoadMetaData();

        OnLocalizationReady?.Invoke();
    }

    ...
}

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

public class LanguageMenu : MonoBehaviour
{
    private bool locaIsReady;

    private void Awake()
    {
        Localization.OnLocalizationReady -= OnLocalizationReady;
        Localization.OnLocalizationReady += OnLocalizationReady;

        Localization.Initialize();
    }

    private void OnDestroy ()
    {
        Localization.OnLocalizationReady -= OnLocalizationReady;
    }

    // This now replaces whatever you wanted to do in Start originally
    private void OnLocalizationReady ()
    {
        locaIsReady = true;

        Debug.Log(Localization.Available.Count);
    }

    private void Update()
    {
        // Block execution until locaIsReady
        if(!locaIsReady) return;

        ...
    }
}

Или для минимально лучшей производительности вы также можете установить enabled = false в Awake и установить значение true в OnLocalizationReady, тогда вы сможете избавиться от флага locaIsReady.


Нет async и await необходимо.


Если бы вы переместили Localization.Initialize(); вместо Start, вы бы дали другим классам возможность также добавить некоторые обратные вызовы до Localization.OnLocalizationReady в Awake;)


И вы можете расширить это несколькими способами! Вы могли бы, например, вместе с инициированием события напрямую также передается ссылка на Availables, чтобы слушатели могли напрямую использовать его, например,

public static class Localization
{
    public static event Action<List<string>> OnLocalizationReady;

    ...
}

, а затем в LanguageMenu изменить подпись OnLocalizationReady на

public class LanguageMenu : MonoBehaviour
{
    ...

    // This now replaces whatever you wanted to do in Start originally
    private void OnLocalizationReady(List<string> available)
    {
        locaIsReady = true;

        Debug.Log(available.Count);
    }
}

Если в любом случае LanguageMenu будет единственным прослушивателем, вы можете даже передать обратный вызов непосредственно в качестве параметра Initialize, например

public static class Localization
{
    public static async void Initialize(Action<List<string>> onSuccess)
    {
        await LoadMetaData();

        onSuccess?.Invoke();
    }

    ...
}

и затем используйте его как

private void Awake()
{
    Localization.Initialize(OnLocalizationReady);
}

private void OnLocalizationReady(List<string>> available)
{
    locaIsReady = true;

    Debug.Log(available.Count);
}

или как лямбда-выражение

private void Awake()
{
    Localization.Initialize(available => 
    {
        locaIsReady = true;

        Debug.Log(available .Count);
    }
}

Обновление

Что касается вашего вопроса о последующей инициализации: Да, есть и простое исправление

public static class Localization
{
    public static event Action OnLocalizationReady;

    public static bool isInitialized;

    public static async void Initialize()
    {
        await LoadMetaData();

        isInitialized = true;
        OnLocalizationReady?.Invoke();
    }

    ...
}

Тогда в других классах вы можете сделать это условно, либо использовать обратные вызовы, либо сразу же инициализировать:

private void Awake()
{
    if(Localization.isInitialized)
    {
        OnLocaInitialized();
    }
    else
    {
        Localization.OnInitialized -= OnLocaInitialized;
        Localization.OnInitialized += OnLocaInitialized;
    }
}

private void OnDestroy ()
{
    Localization.OnInitialized -= OnLocaInitialized;
}

private void OnLocaInitialized()
{
    var available = Localization.Available;

    ...
}

private void Update()
{
    if(!Localization.isInitialized) return;

    ...
}
...