Эта статья написана, чтобы помочь любому разработчику, столкнувшемуся с ошибками, связанными с определениями в коде. А именно, для тех из вас, кто выполнял какое-то переименование через IDE и вдруг получил ошибку времени компиляции (CTE) в релизной сборке. Прочитав эту статью, вы будете использовать менее подверженные ошибкам методы при использовании определений, что позволит вам сталкиваться с меньшим количеством случаев CTE.

Простое использование определяет

Рассмотрим простой случай использования

using UnityEngine;
using UnityEditor;
//...
private void Start()
{
    DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
    AssetDatabase.SaveAssets();
#endif
}

Проблемы

Приведенный выше пример демонстрирует, как мы не можем скомпилировать этот код ни в одной сборке. Этот код будет работать только в редакторе.

Этот метод использует класс AssetDatabase, объявленный в UnityEditor. Конечно, чтобы использовать его, мы должны объявить использование UnityEditor в начале файла.

Здесь наша основная проблема заключается в использовании того, что ваша IDE обычно импортирует в файл. Это может хорошо работать в обычных случаях, но не в том случае, если вы используете определения. В данном конкретном случае мы импортировали UnityEditor в файл, которого нет в сборках. Вот почему мы не можем скомпилировать этот пример для любой сборки, мобильного телефона, консоли, ПК и т. д.

Вариант решения

Теперь, когда мы понимаем проблемы, пришло время их решить.

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
//...
private void Start()
{
    DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
    AssetDatabase.SaveAssets();
#endif
}

Он компилируется, но здесь у вас есть еще одно определение использования. Если вы реализуете какое-либо дополнительное использование, то оно все равно будет автоматически вынесено из определения и снова приведет к CTE.

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using Reimporter.AvailableOnlyInEditor;

//...
private void DoSmthEditor()
{
#if UNITY_EDITOR
    AssetDatabase.SaveAssets();
    Reimporter.ReimportAllFolders();
#endif
}

Ясно, что это потенциальное решение не так безопасно. Как мы можем улучшить его?

Случай с полным пространством имен

using UnityEngine;
//...

private void Start()
{
    DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
    UnityEditor.AssetDatabase.SaveAssets();
#endif
}

В этом примере мы используем полное имя класса.

Не оставляйте пространство имен using поверх файла так, как это было предложено вашей IDE. Просто используйте полное имя классов внутри ваших определений, чтобы в этом случае не использовать CTE.

Проблемы

Этот подход почти не имеет проблем, за исключением следующего:

1) В случаях с более чем 10–20 строками может быть сложно соблюдать это правило.

2) Он все равно не переименует его безопасно. Это связано с тем, что если в текущем контексте отключены определения, то код под отключенными определениями не активен. Он не будет вызывать IDE; это просто текст, как комментарий. Как только этот код будет активирован путем включения определения, вы обнаружите, что у вас есть CTE.

Условный

Можно ли разрешить и этот последний случай? Да.

using UnityEngine;
//...
private void Start()
{
    DoSmthEditor();
}

[Conditional("UNITY_EDITOR")]
private void DoSmthEditor()
{
    UnityEditor.AssetDatabase.SaveAssets();
}

При использовании атрибута [Conditional()] у вас всегда будет код, доступный в IDE, даже если ваши определения не активны.

Таким образом, вам будет проще анализировать код, отображать предупреждения, вносить предложения и выполнять рефакторинг. Это верно и в случае переименования.

Проблемы

Этот подход может быть очень полезным и предпочтительным в большинстве случаев, но он также имеет ограничения:

- Метод должен возвращать void

- Он не поддерживает операции «или», такие как «UNITY_EDITOR || UNITY_IOS’

Бонус — подсказка регистратора

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

Мы можем легко сделать это следующим образом:

public void LogTrace(string message)
{
#if LOGLEVEL_TRACE
    Log(LogLevel.Trace, message)
#endif
}

Это работает, но это также представляет проблему. Метод LogTrace по-прежнему компилируется и выполняется со строками, созданными при вызове. И строки для создания журнала, который все еще создается для каждого не включенного журнала, генерируют мусор в куче. т. е. любые конкатенации строк или форматирование все равно будут иметь место.

Просто используйте атрибут [Conditional] для повышения производительности вашего проекта следующим образом:

[Conditional("LOGLEVEL_TRACE")]
public void LogTrace(string message)
{
    Log(LogLevel.Trace, message)
}

Ограничение проекта

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

Для получения дополнительной информации ознакомьтесь с Документацией Unity.

Заключение

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

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

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

Например,

public class LoggerProvider : ILoggerProvider
{
    public ILogger GetLogger(string category)
    {
    #if LOGS_ENABLES
        return new Logger(category);
    #endif
        return new NullLogger();
    }
}

Или вы можете использовать только одно определение и иметь прозрачную логику в своем выделенном классе:

void InstallBindings()
{
#if LOGS_ENABLED
    Container.BindAllInterfacesTo<LoggerProvider>().AsSingle();
#else
    Container.BindAllInterfacesTo<NullLoggerProvider>().AsSingle();
#endif
}

public class NullLoggerProvider : ILoggerProvider
{
    public ILogger GetLogger(string category)
    {
        return new NullLogger();
    }
}

public class LoggerProvider : ILoggerProvider
{
    public ILogger GetLogger(string category)
    {
        return new Logger(category);
    }
}

Напишите нам, если вы увлечены созданием игр и хотите познакомиться с одной из самых динамичных игровых культур на сегодняшний день [email protected]