Думаю, что к этому мы ещё придём.
А для транзактных изменений у нас тоже есть инструменты, но используются они явно. Код в статье слишком упрощён, я не стал там показыать всё. Делается как-то так
using (viewModel.Lock())
{
// тут мы меняем свойста
} // тут срабатывают все затронутые диспатчеры
1) Согласен. Есть такой план, даже расширенный – уметь ещё и десериализовать ViewModel. Очень пригодится для автотестов интерфйса. Автотест загружает префаб, пихает в нго условный джсон и смотрит, что получилось. Огромный плюс в том, что при реализации такого тестирования можно максимально абстрагироваться от кода самого проекта.
2) Да, возможно это зло. Но почти за два года использования связанных с этим проблем возникло исчезающе мало. Я подумываю, что тут можно сделать, но меня останавливает нежелание чинить то, что не сломано.
Строго говоря константы там не строковые, ключи имеют тип PropertyName обычно их значениня уносятся в константы. Хотя есть желание отказаться от этого в пользу обычных строк. Но я так понимаю, вы не можете «простить» именно «динамичность» вьюмодели? Если так, то дискуссия рискует перетечь в плоскость очердного обсуждения «статика vs динамика». Мы и сами туда часто скатывамся во внутрннних обсуждениях. Но тут был упор именно на то, что набор свойств без особых усилий сформировать может человек, не пишущий код.
Валидация – это хорошо, и мы добавляем проверки, особенно в критичных частях. Но это касается и первого куска кода, который с линковкой компонент в поля, так что тут описанный подход ничем особенным не выделяется.
Дорабатывать планируем. Будет расширение разнообразия типов (очень в этом поможет SerializeReference) и прочие плюшки. А валидация (и даже некоторая кодогенерация) уже частично реализованы.
По функциональности мы покрыли всё, что есть у других участников сравнения, за исключениеем каких-то специфичных вещей, вытекающих из особенностей рализации.
Смотря что вы имеете ввиду под Dependency Injection. Если вы про DI-контейнер, то да, некоторые проекты используют Zenject, но не все (один, если быть точным). А если про более общее понятие "внедрение зависимостей", то тоже да, конечно же.
Что касается моей точки зрения, то я сторонник как можно более явного подхода к управлению зависимостями. Просто не очень люблю магию метапрограммирования в рантайме (это я про C# в частности). Я за так называемый Poor Man's DI.
Можно узнать, как вас подтолкнула тема статьи к этому вопросу?)
Я сейчас попробую ответить, возможно немного сумбурно получится.
Префаб обходится просто – берём все копоненты на всех GO внутри и вызываем валидацию на каждом. Со сценами сложнее, сцену нужно загрузить (EditorSceneManager.OpenScene), а потом пройтись по GetRootGameObjects так же, как по префабам. Ещё мы проверяем ассеты ScriptableObject.
Тут есть подводный камень – ассеты ScriptableObject могут быть вложены в любые другие ассеты.
Далее главное синхронизировать обход по SerializedObject и обход рефлексией вглубь по типу объекта.
Берём каждый компонент или скруптуемый объект и обходим его
var serializedObject = new SerializedObject(unityObject);
Enumerate(unityObject, serializedObject.GetIterator());
Вот так примерно выглядит сам обход
IEnumerable<ValueTuple<SerializedProperty, FieldInfo>> EnumerateProperties(object obj, SerializedProperty property)
{
// и идём по property
if (obj == null || !property.Next(true)) yield break; // это был пустой объект
do
{
var fieldInfo = unityObject.GetType().GetFieldInfo(property.name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // *
if (fieldInfo == null) continue;
yield return (property.Copy(), fieldInfo); // отдаём на проверку
// если это референс на другой объект, то вглубь идти не нужно, пропускаем
if (property.propertyType != SerializedPropertyType.Generic) continue;
if (property.isArray)
{
var array = fieldInfo.GetValue(unityObject) as IEnumerable; // не забываем проверить на null, бывают такие случаи
var index = 0;
foreach (var element in array)
{
var item = property.GetArrayElementAtIndex(index++);
yield return (item.Copy(), fieldInfo); // отдаём на проверку элемент массива
if (item.propertyType != SerializedPropertyType.Generic) continue; // опять же, не проверяем ссылки
foreach (var pair in Enumerate(element, item.Copy())) yield return pair;
}
continue; // с массивом покончили
}
// тут просто поле, и его нужно вглубь
foreach (var pair in Enumerate(fieldInfo.GetValue(obj), property.Copy())) yield return pair;
}
while (property.Next(false));
}
Примечание к * в коде:
Тут на самом деле нужно хитрее, пройтись по всей иерархии типа наверх (BaseType), пока не упрёшься в типы, которые уже не нужно проверять. Это как MonoBehaviour, SerializedObject и прочие основные типы, так и thirdparty компоненты, в которые вы не можете вставлять аттрибуты валидации. Это один подход. Другой подход – это заранее подготовить список типов, в которых есть аттрибуты валидации и искать только в них.
Тут дело в том, что двусвязанный список не оперирует индексами, он использует связи.
Именно связи. Связи != указатели. И указатели в принципе – это тоже индексы, смещение которых скрывает от вас контекст (рантайм). Здесь просто более явный контекст – конкрретный экземпляр DLL.
Все удобство и все свойства двусвязанного списка заключаются в использовании узла, который знает о соседних узлах, которые в свою очередь могут находиться в памяти где угодно.
Да, для данного конкретного юзкейса DLL предложенный вариант не подходит. Но не все так работают со списком.
Например такой сценарий: вам нужно просто получить данные, скажем, 2762 элемента списка. Вы просто итерируете 2762 раз вперед от начала (не подгружая ненужные поля) и забираете данные по индексу из массива с данными.
Для такого сценария предложенный вариант будет эффективнее.
Структура это не просто интерфейс, важно ее устройство.
В том-то и дело, что устройство логически не поменялось, поменялся просто лейаут. Он из вертикального (AoS) превратился в горизонтальный (SoA). Мне очень часто приходится так делать, для разделения горячих и холодных данных, например.
Заметьте так же, что даже алгоритмы вставки в примере не так уж отличаются от примеров в той же википедии.
Проблема дыр — это основная проблема Вашего способа — простое запоминание индексов дыр тут не поможет — для быстродействия это ничто, но в плане памяти — нужно помнить обо всех дырах, что в случае большого capacity очень накладно.
И тут я могу обойтись без дыр с помощью свопа из конца в удалённую позицию. Жертвуя валидностью итератора. Который, впрочем, я могу вернуть из метода удаления обновлённым и дейсвтительным.
Соответственно структуру лучше выбирать, когда известна задача — статья больше полезна для разминки мозгов, за что автору спасибо.
Всё, кроме этого вывода в статье – детали. Спасибо, что обратили внимание на главное
Вы правы, при вставке кортежа тут будет O(размер кортежа). Но для подобной задачи я бы что-нибудь иное придумал. Первое, что приходит в голову – делить эти массивы между списками, с которыми так придётся поступить (смержить).
Опять же, всё зависит от задачи, не всегда есть возможность написать эффективное общее решение.
Как вы быстро и безапелляционно отмели разговоры о кеш-линиях и не дали права называться этому двусвязным списком.
Если это снаружи выглядит для пользователя как список, ведёт себя как список – то это список.
Да, вариант с флажком тоже имеет право на существование, кто-то мне его предлагал, но это показалось мне слишком простым. Плюс интепрпретация указателей в зависимости от состояния этого флажка – ещё один уровень перенаправления (level of indirection). В виде бранча (return is_reversed? link1: link2) или в виде взятия по индексу (return links[is_reversed]). Не сказал бы, что это решение будет «чище».
Кто-то даже предлагал редактировать виртуальную таблицу (!) ноды, но это уж совсем экзотика
И меня это тоже смутило, я и сам не понимаю. Сама постановка задачи. Но я её встречал множество раз со времён университета. Меня смутило само решение, которое все пишут одинаково.
Ну и ещё, кстати, в стандарте есть такой метод: en.cppreference.com/w/cpp/container/list/reverse и он работает за O(N)
Приведённый код – это всго лишь proof of concept, а не полноценная реализация. Я упомянул это в тексте. Можно убрать этот лимит и увеличивать капасити массивов при переполнении. И да, в некоторых случаях это может повлиять на время добавления нового узла. Иногда нет. Всё опять же зависит от ваших данных, и от задачи. Иногда вы просто знаете капасити наперёд.
Думаю, что к этому мы ещё придём.
А для транзактных изменений у нас тоже есть инструменты, но используются они явно. Код в статье слишком упрощён, я не стал там показыать всё. Делается как-то так
1) Согласен. Есть такой план, даже расширенный – уметь ещё и десериализовать ViewModel. Очень пригодится для автотестов интерфйса. Автотест загружает префаб, пихает в нго условный джсон и смотрит, что получилось. Огромный плюс в том, что при реализации такого тестирования можно максимально абстрагироваться от кода самого проекта.
2) Да, возможно это зло. Но почти за два года использования связанных с этим проблем возникло исчезающе мало. Я подумываю, что тут можно сделать, но меня останавливает нежелание чинить то, что не сломано.
Строго говоря константы там не строковые, ключи имеют тип PropertyName обычно их значениня уносятся в константы. Хотя есть желание отказаться от этого в пользу обычных строк. Но я так понимаю, вы не можете «простить» именно «динамичность» вьюмодели? Если так, то дискуссия рискует перетечь в плоскость очердного обсуждения «статика vs динамика». Мы и сами туда часто скатывамся во внутрннних обсуждениях. Но тут был упор именно на то, что набор свойств без особых усилий сформировать может человек, не пишущий код.
Валидация – это хорошо, и мы добавляем проверки, особенно в критичных частях. Но это касается и первого куска кода, который с линковкой компонент в поля, так что тут описанный подход ничем особенным не выделяется.
Дорабатывать планируем. Будет расширение разнообразия типов (очень в этом поможет SerializeReference) и прочие плюшки. А валидация (и даже некоторая кодогенерация) уже частично реализованы.
По функциональности мы покрыли всё, что есть у других участников сравнения, за исключениеем каких-то специфичных вещей, вытекающих из особенностей рализации.
Смотрели. Если кратко, то в UniRx нет динамических ViewModel, из-за чего авторинг блокируется программистами
Смотря что вы имеете ввиду под Dependency Injection. Если вы про DI-контейнер, то да, некоторые проекты используют Zenject, но не все (один, если быть точным). А если про более общее понятие "внедрение зависимостей", то тоже да, конечно же.
Что касается моей точки зрения, то я сторонник как можно более явного подхода к управлению зависимостями. Просто не очень люблю магию метапрограммирования в рантайме (это я про C# в частности). Я за так называемый Poor Man's DI.
Можно узнать, как вас подтолкнула тема статьи к этому вопросу?)
Я сейчас попробую ответить, возможно немного сумбурно получится.
Префаб обходится просто – берём все копоненты на всех GO внутри и вызываем валидацию на каждом. Со сценами сложнее, сцену нужно загрузить (EditorSceneManager.OpenScene), а потом пройтись по GetRootGameObjects так же, как по префабам. Ещё мы проверяем ассеты ScriptableObject.
Тут есть подводный камень – ассеты ScriptableObject могут быть вложены в любые другие ассеты.
Далее главное синхронизировать обход по SerializedObject и обход рефлексией вглубь по типу объекта.
Примечание к * в коде:
Тут на самом деле нужно хитрее, пройтись по всей иерархии типа наверх (BaseType), пока не упрёшься в типы, которые уже не нужно проверять. Это как MonoBehaviour, SerializedObject и прочие основные типы, так и thirdparty компоненты, в которые вы не можете вставлять аттрибуты валидации. Это один подход. Другой подход – это заранее подготовить список типов, в которых есть аттрибуты валидации и искать только в них.
Тут вам уже немного наотвечали. Я думаю, что стоит посвятить этому отдельный пост в будущем.
Это, конечно, элегантно) Только, во-первых, у вас опечатка
Во вторых, вам придётся писать по две реализации любой функции, которая принимает ваш список.
Но попытка хороша)
Не стал писать remove потому, что это не имело отнишения к делу. Специально для вас быстренько накидал. Без дырок. Можно лучше, но быстренько вот так:
Ответ на этот комментарий и на комментарий выше.
Именно связи. Связи != указатели. И указатели в принципе – это тоже индексы, смещение которых скрывает от вас контекст (рантайм). Здесь просто более явный контекст – конкрретный экземпляр DLL.
Да, для данного конкретного юзкейса DLL предложенный вариант не подходит. Но не все так работают со списком.
Например такой сценарий: вам нужно просто получить данные, скажем, 2762 элемента списка. Вы просто итерируете 2762 раз вперед от начала (не подгружая ненужные поля) и забираете данные по индексу из массива с данными.
Для такого сценария предложенный вариант будет эффективнее.
В том-то и дело, что устройство логически не поменялось, поменялся просто лейаут. Он из вертикального (AoS) превратился в горизонтальный (SoA). Мне очень часто приходится так делать, для разделения горячих и холодных данных, например.
Заметьте так же, что даже алгоритмы вставки в примере не так уж отличаются от примеров в той же википедии.
И тут я могу обойтись без дыр с помощью свопа из конца в удалённую позицию. Жертвуя валидностью итератора. Который, впрочем, я могу вернуть из метода удаления обновлённым и дейсвтительным.
Всё, кроме этого вывода в статье – детали. Спасибо, что обратили внимание на главное
Опять же, всё зависит от задачи, не всегда есть возможность написать эффективное общее решение.
Если это снаружи выглядит для пользователя как список, ведёт себя как список – то это список.
Да, вариант с флажком тоже имеет право на существование, кто-то мне его предлагал, но это показалось мне слишком простым. Плюс интепрпретация указателей в зависимости от состояния этого флажка – ещё один уровень перенаправления (level of indirection). В виде бранча (return is_reversed? link1: link2) или в виде взятия по индексу (return links[is_reversed]). Не сказал бы, что это решение будет «чище».
Кто-то даже предлагал редактировать виртуальную таблицу (!) ноды, но это уж совсем экзотика
Ну и ещё, кстати, в стандарте есть такой метод: en.cppreference.com/w/cpp/container/list/reverse и он работает за O(N)