Эта статья написана, чтобы помочь любому разработчику, столкнувшемуся с ошибками, связанными с определениями в коде. А именно, для тех из вас, кто выполнял какое-то переименование через 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]