Здравствуй, Хабр! На написание данной статьи меня подтолкнуло то, что на данный момент я не нашёл ни одной реализации вызова методов без словаря в открытом доступе. Более того, на двух форумах нашлись люди, которые утверждают о невозможности подобной реализации, а потому, спешу опровергнуть это.
Проект реализован на Unity версии 2021.2.7f1
Введение или как я учился гуглить ;)
Я человек ленивый, мне была необходима консоль для тестирования методов, и я не хотел постоянно тратить время на пополнение словаря. Вызов void функции я создал быстро, но вот дальше не всё сразу пошло по плану. Изначально я не сильно вдавался в терминологию, а потому мой вопрос гуглу звучал:"Как вызвать функцию с аргументами в юнити?". Я уверен, что любой программист понял бы, о чём идёт речь, но вот гугл меня не понял. Спустя страниц 15 документаций и сайтов я, наконец, смог правильно сформулировать вопрос:"Как вызвать метод с параметрами в Unity?". Очень сильно в этом вопросе мне помог источник, приведённый ниже.
Параметры методов
После изучения существующих решений, я понял, что меня интересует рефлексия.
Что такое рефлексия?
Подробнее об этом можно прочитать здесь.
https://metanit.com/sharp/tutorial/14.1.php
Краткое пояснение принципа работы консоли
Хочется для начала вкратце пояснить принцип работы этой консоли. Мы ведь можем получить все объекты на сцене и перебрать их, можем получить классы, которые навешаны на данные объекты, ведь большинство, если не все классы, которые вы пишете унаследованы от MonoBehaviour. Что мешает отыскать нужный метод просто по имени? Вот и выходит, что вся консоль основана всего лишь на двух строках
MonoBehaviour[] monobehs = setgo.GetComponents<MonoBehaviour>();
//получаем классы
//унаследованные от MonoBehaviour
mb.GetType().GetMethod(func_name).Invoke(mb, args_obj);
//получаем метод в классе mb
//унаследованном от MonoBehaviour и
//вызываем его, отправляя массив аргументов args_obj
Что касается вывода ошибок, здесь всё ещё проще. Существует ивент юнити, который позволяет получить все сообщения, выводимые в консоль самого движка.
Application.logMessageReceived += OnLogMessageReceived;
Вот таким образом мы подключаем метод в качестве слушателя. Сам же метод принимает следующие значения:
private void OnLogMessageReceived(string condition,string stacktrace, LogType type)
{...}
Более подробно об этом слушателе вы можете прочитать здесь, именно отсюда я его и взял:
https://stackoverflow.com/questions/44376919/logger-for-unity3d-that-hooks-up-nicely-with-monodevelop
Подробный разбор консоли
Начнём с класса OnError, который и открывает нашу консольку при возникновении каких-либо ошибок или сообщений в консоли самого движка.
OnError
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class OnError : MonoBehaviour
{
public ConstructEvents CE;
public bool PauseOnRed;
public bool PauseOnYellow;
public bool PauseOnLog;
public Pause pause;
public GameObject console;
public GameObject content_scroll;
public GameObject message;
public List<string> all_msg_str;
public bool togglebool;
public Toggle tggl;
private void Awake()
{
//изначально мы должны подключить слушателя консоли Unity
Application.logMessageReceived += OnLogMessageReceived;
//EventManager создаёт лист из UnityEvent и его названия
//так мы можем создать 3 ивента, на 3 разных вывода:
//жёлтое предупреждение, красная ошибка или серое сообщение
CE = GameObject.FindGameObjectWithTag("EventManager").GetComponent<ConstructEvents>();
// pause позволяет установить скорость течения времени,
//в том числе и на 0, т.е. паузу.
pause = GameObject.FindGameObjectWithTag("TimeManager").GetComponent<Pause>();
//эта переменная позволяет узнать состояние галочки
//от которой будет зависеть, будет ли консоль отображать
//сообщения заподряд, или только при нажатии на соответствующие кнопки
togglebool = tggl.isOn;
}
public void onToggleClick()
{
togglebool = tggl.isOn;
}
public void log()
{
//вынужденная мера: на время вывода сообщений в консоль,
//мы отключаем ивент, чтобы не уйти в бесконечный цикл
Application.logMessageReceived -= OnLogMessageReceived;
foreach (var i_msg in all_msg_str)
{
if (i_msg.Contains("Log") || i_msg.Contains("Assert"))
{
MessageAtScroll(i_msg);//это функция отправки сообщения в консоль
}
}
//Сообщения выведены, можно вернуть ивент
Application.logMessageReceived += OnLogMessageReceived;
}
public void clear()//очистка консоли
{
foreach (Transform child in content_scroll.transform)
{
Destroy(child.gameObject);
}
}
public void warning()//вывод всех предупреждений
{
Application.logMessageReceived -= OnLogMessageReceived;
foreach (var i_msg in all_msg_str)
{
if (i_msg.Contains("Warning"))
{
MessageAtScroll(i_msg);
}
}
Application.logMessageReceived += OnLogMessageReceived;
}
public void error()//вывод всех ошибок
{
Application.logMessageReceived -= OnLogMessageReceived;
foreach (var i_msg in all_msg_str)
{
if (i_msg.Contains("Error") || i_msg.Contains("Exception"))
{
MessageAtScroll(i_msg);
}
}
Application.logMessageReceived += OnLogMessageReceived;
}
public void MessageAtScroll(string msg_str)
//отправка сообщения в нашу консольку
{
GameObject str = Instantiate(message);//создаём текст
str.GetComponent<Text>().text = msg_str;//изменяем текст на полученное сообщение
//устанавливаем родителя-скролл консольки для текста
str.GetComponent<Transform>().parent = content_scroll.transform;
//эти 3 строки нужны для правильного отображения текста
str.GetComponent<RectTransform>().position = content_scroll.GetComponent<RectTransform>().position;
str.GetComponent<RectTransform>().rotation = content_scroll.GetComponent<RectTransform>().rotation;
str.GetComponent<RectTransform>().localScale = content_scroll.GetComponent<RectTransform>().localScale;
}
private void OnLogMessageReceived(string condition,string stacktrace, LogType type)
{//класс-слушатель консольки движка
if (!Application.isPlaying) return;//отключаем его, если
//игра не запущена
//далее мы стараемся сделать так,
//чтобы не выдавались дублирующиеся сообщения
bool dublicate=false;
foreach (var i_msg in all_msg_str)
{
if (i_msg.Equals(type.ToString() + ": " + condition))
{
dublicate = true;
}
}
if (!dublicate)
{
all_msg_str.Add(type.ToString() + ": " + condition);
}
if (togglebool) {MessageAtScroll(type.ToString() + ": " + condition);}
switch (type)
{//останавливаем игру и открываем консоль
case LogType.Error:
CE.FindUnityEventByName("ErrorManager.OnLogMessageReceived.OnRedError").Invoke();
if (PauseOnRed)
{
pause.SetWorldTime(0);//ставим игру на паузу
console.SetActive(true);
}
break;
case LogType.Warning:
CE.FindUnityEventByName("ErrorManager.OnLogMessageReceived.OnYellowError").Invoke();
if (PauseOnYellow)
{
pause.SetWorldTime(0);//ставим игру на паузу
console.SetActive(true);
}
break;
case LogType.Log:
CE.FindUnityEventByName("ErrorManager.OnLogMessageReceived.OnLog").Invoke();
if (PauseOnLog)
{
pause.SetWorldTime(0);//ставим игру на паузу
console.SetActive(true);
}
break;
}
}
}
Итак, в рассмотренном ранее классе появился класс EventManager, и хоть я и кратко описал его суть в комментариях к коду, представим его:
ConstructEvents
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class ConstructEvents : MonoBehaviour
{
public List<ConstructEv> EventsList;
public void InvokeEvent(string eventname)
{
for (int i = 0; i < EventsList.Count; i++)
{
if (EventsList[i].EventName == eventname)
{//вызываем UnityEvent по его названию
EventsList[i].UnityEv.Invoke();
}
}
}
public UnityEvent FindUnityEventByName(string name)
{//поиск UnityEvent по его названию
Debug.Log(name);
for (int i = 0; i < EventsList.Count; i++)
{
if (EventsList[i].EventName == name)
{
return EventsList[i].UnityEv;
}
}
Debug.Log("Ивент "+name+" не был найден по имени, проверьте правильность написания имени события");
return null;
}
[System.Serializable]
public struct ConstructEv
{
public UnityEvent UnityEv;
public string EventName;
public ConstructEv(UnityEvent UnityEv, string EventName)
{
this.EventName = EventName;
this.UnityEv = UnityEv;
}
}
}
Совсем малюсенький класс паузы:
Pause
using UnityEngine;
public class Pause : MonoBehaviour
{
public float time;
public void SetWorldTime(float settime)
{
Time.timeScale = settime;
//устанавливаем скорость течения ремени
}
}
И прежде чем перейти к гвоздю программы, я хочу рассмотреть класс FindManager, который ищет GameObject на сцене по имени и тегу или префаб в ресурсах игры, который обязательно должен храниться в папке под названием Resources (Подробнее об этом можно прочитать в официальной документации Unity https://docs.unity3d.com/ScriptReference/Resources.html).
FindManager
using UnityEngine;
public class FindManager : MonoBehaviour
{
public static GameObject FindGO(string go_name)//ищет и по тегу и по имени
{
GameObject go = FindGO_select(go_name, true, true, true);
return go;
}
public static GameObject FindGO_select(string go_name,bool IsFindName,bool IsFindTag,bool IsFindResources)
{
GameObject setgo=null;
if (IsFindName)
{
setgo = GameObject.Find(go_name); // поиск объекта по имени
}
if (setgo == null)
{
if (IsFindTag)
{
string[] tag_mass = UnityEditorInternal.InternalEditorUtility.tags;
bool tagnotnull = false;
foreach (string i_tag in tag_mass)
{
if (go_name.Equals(i_tag))
{
tagnotnull = true;
break;
}
}
if (tagnotnull)
{
setgo = GameObject.FindGameObjectWithTag(go_name); // поиск объекта по тегу
}
}
if (setgo == null)
{
if (IsFindResources)
{
setgo = Resources.Load(go_name, typeof(GameObject)) as GameObject;// поиск префаба в ресурсах
}
}
}
return setgo;
}
}
И наконец, ОН - класс Console. Именно он отвечает за вызов метода, в нём происходит преобразование вводимых аргументов в различные типы данных. Данный класс может вызвать методы имеющие в параметрах как обычные типы данных C# и GameObject, так и их массивы и List-ы
Console
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.Linq;
public class Console : MonoBehaviour
{
public UnityEvent ue_vvod;
public UnityEvent ue_play;
public string StringInput;
public Text input_text;
public GameObject setgo;
public OnError OnErr;
public List<object> dyn_type_list = new List<object>();
public List<Type> par_type = new List<Type>();
public string func_name;
public int num_vvod_par=0;
public bool vvod_par_bool=false;
MonoBehaviour mb;
public void CallFuncWithArgs(string func_name, List<object> args, List<Type> musttype)
{
int kolvo_args = args.Count;
for (int i = 0; i < kolvo_args; i++)
{
if (musttype[i].Equals(typeof(int)))
{
args[i] = Convert.ToInt32(args[i]);
}
else if (musttype[i].Equals(typeof(string)))
{
args[i] = Convert.ToString(args[i]);
}
else if (musttype[i].Equals(typeof(bool)))
{
args[i] = Convert.ToBoolean(args[i]);
}
else if (musttype[i].Equals(typeof(float)))
{
args[i] = Convert.ToSingle((args[i]));
}
else if (musttype[i].Equals(typeof(byte)))
{
args[i] = Convert.ToByte((args[i]));
}
else if (musttype[i].Equals(typeof(char)))
{
args[i] = Convert.ToChar((args[i]));
}
else if (musttype[i].Equals(typeof(DateTime)))
{
args[i] = Convert.ToDateTime((args[i]));
}
else if (musttype[i].Equals(typeof(decimal)))
{
args[i] = Convert.ToDecimal((args[i]));
}
else if (musttype[i].Equals(typeof(double)))
{
args[i] = Convert.ToDouble((args[i]));
}
else if (musttype[i].Equals(typeof(int[])))
{
//string[] str = Convert.ToString(args[i]).Split("/;");
int[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => int.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(string[])))
{
string[] str = Convert.ToString(args[i]).Split("/;");
args[i] = str;
}
else if (musttype[i].Equals(typeof(bool[])))
{
bool[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => bool.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(float[])))
{
float[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => float.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(byte[])))
{
byte[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => byte.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(char[])))
{
char[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.None).Select(x => char.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(DateTime[])))
{
DateTime[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => DateTime.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(decimal[])))
{
decimal[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => decimal.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(double[])))
{
double[] str = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => double.Parse(x)).ToArray();
args[i] = str;
}
else if (musttype[i].Equals(typeof(List<int>)))
{
List<int> int_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => int.Parse(x)).ToArray().Cast<int>().ToList();
args[i] = int_list;
}
else if (musttype[i].Equals(typeof(List<string>)))
{
List<string> str_list = Convert.ToString(args[i]).Split("/;").ToArray().Cast<string>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<bool>)))
{
List<bool> str_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => bool.Parse(x)).ToArray().Cast<bool>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<float>)))
{
List<float> str_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => float.Parse(x)).ToArray().Cast<float>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<byte>)))
{
List<byte> str_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => byte.Parse(x)).ToArray().Cast<byte>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<char>)))
{
List<char> str_list = Convert.ToString(args[i]).Split("/;").ToArray().Cast<char>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<DateTime>)))
{
List<DateTime> str_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => DateTime.Parse(x)).ToArray().Cast<DateTime>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(List<decimal>)))
{
List<decimal> str_list = Convert.ToString(args[i]).Split(new[] { "/;" }, StringSplitOptions.RemoveEmptyEntries).Select(x => decimal.Parse(x)).ToArray().Cast<decimal>().ToList();
args[i] = str_list;
}
else if (musttype[i].Equals(typeof(GameObject)))
{
args[i] = FindManager.FindGO(Convert.ToString(args[i]));
}
else if (musttype[i].Equals(typeof(GameObject[])))
{
object[] str = Convert.ToString(args[i]).Split("/;");
for(int i_str=0;i < str.Length;i++)
{
str[i_str] = FindManager.FindGO(Convert.ToString(str[i_str]));
}
args[i] = str;
}
else if (musttype[i].Equals(typeof(List<GameObject>)))
{
object[] str = Convert.ToString(args[i]).Split("/;");
for (int i_str = 0; i < str.Length; i++)
{
str[i_str] = FindManager.FindGO(Convert.ToString(str[i_str]));
}
args[i] = ConvertTypes.ObjMassTo_GO_List(str);
}
}
object[] obj_mass;
obj_mass = new object[kolvo_args];
for (int i = 0; i < kolvo_args; i++)
{
obj_mass[i] = args[i];
}
object[] args_obj = args.ToArray();
mb.GetType().GetMethod(func_name).Invoke(mb, args_obj);
clear();
OnErr.MessageAtScroll("Метод выполнен");
}
public void clear()
{
setgo = null;
OnErr.MessageAtScroll("Объект установлен на null");
num_vvod_par = 0;
vvod_par_bool = false;
par_type = new List<Type>();
dyn_type_list = new List<object>();
mb = null;
}
public void vvod()
{
StringInput = input_text.text;
OnErr.MessageAtScroll("Введено:"+StringInput);
input_text.text = "";
switch (StringInput)
{
case "null":
clear();
break;
default:
if (vvod_par_bool)
{
OnErr.MessageAtScroll("Параметр был введён");
dyn_type_list[num_vvod_par] = StringInput;
num_vvod_par++;
if (num_vvod_par >= dyn_type_list.Count)
{
OnErr.MessageAtScroll("Все параметры были введены. Выполнение метода...");
CallFuncWithArgs(func_name, dyn_type_list, par_type);
}
return;
}
if (setgo == null)
{
setgo = FindManager.FindGO(StringInput);
if (setgo != null)
{
OnErr.MessageAtScroll("Объект " + StringInput + " найден");
}
else
{
OnErr.MessageAtScroll("Объект " + StringInput + " не найден");
}
}
else
{
call_func(setgo, StringInput);
}
break;
}
ue_vvod.Invoke();
}
public void play()
{
ue_play.Invoke();
}
public void call_func(GameObject setgo,string namefunc)
{
MonoBehaviour[] monobehs = setgo.GetComponents<MonoBehaviour>();
bool isfind = false;
foreach (var i_monobeh in monobehs)
{
if (i_monobeh.GetType().GetMethod(namefunc) != null)
{
OnErr.MessageAtScroll("Метод найден в классе " + i_monobeh.name);
isfind = true;
int count_args = i_monobeh.GetType().GetMethod(namefunc).GetParameters().Length;
if (count_args <= 0)
{
OnErr.MessageAtScroll("Метод " + StringInput + " вызван");
setgo.BroadcastMessage(namefunc);
}
else
{
mb = i_monobeh;
OnErr.MessageAtScroll("Метод " + StringInput + " имеет параметры в количестве:"+ count_args);
func_name = StringInput;
OnErr.MessageAtScroll("Названия параметров и их тип данных:");
ParameterInfo[] parinf = i_monobeh.GetType().GetMethod(namefunc).GetParameters();
foreach(var i_parinf in parinf)
{
OnErr.MessageAtScroll(i_parinf.Name +" "+i_parinf.ParameterType.ToString());
par_type.Add(i_parinf.ParameterType);
}
while(dyn_type_list.Count<count_args)
{
object a=new object();
dyn_type_list.Add(a);
}
vvod_par_bool = true;
OnErr.MessageAtScroll("Введите параметры:");
}
break;
}
}
if (!isfind)
{
OnErr.MessageAtScroll("Данный метод не найден");
}
}
}
В классе Console использовался класс для преобразования типов данных ConvertTypes, вот и он:
ConvertTypes
using System.Collections.Generic;
using UnityEngine;
public class ConvertTypes : MonoBehaviour
{
public static List<GameObject> ObjListTo_GO_List(List<object> ObjList)
{
List<GameObject> result=null;
for (int i = 0; i < ObjList.Count; i++)
{
result.Add((GameObject)ObjList[i]);
}
return result;
}
public static List<GameObject> ObjMassTo_GO_List(object[] ObjList)
{
List<GameObject> result = null;
for (int i = 0; i < ObjList.Length; i++)
{
result.Add((GameObject)ObjList[i]);
}
return result;
}
public static GameObject[] ObjMassTo_GO_Mass(object[] ObjList)
{
GameObject[] result = null;
for (int i = 0; i < ObjList.Length; i++)
{
result[i] = ((GameObject)ObjList[i]);
}
return result;
}
public static GameObject[] ObjListTo_GO_Mass(List<object> ObjList)
{
GameObject[] result = null;
for (int i = 0; i < ObjList.Count; i++)
{
result[i] = ((GameObject)ObjList[i]);
}
return result;
}
}
Исходники проекта
Исходник на Яндекс диске:
https://disk.yandex.ru/d/a_skfpuPRFofZw
Тот же исходник на Google диске:
https://drive.google.com/file/d/1LnfWg8yMgH7s1OR8D473QqTnn8OZo9F1/view?usp=sharing
Инструкция эксплуатации консоли
Введите имя или тег объекта, в котором находится метод, который вам необходимо выполнить. GameObject будет искаться на сцене или в ресурсах проекта.
Введите имя метода, который хотите выполнить.
Введите параметры метода. При этом ввод данных типа List или массив происходит в одну строку через /;
Всем спасибо за внимание! Желаю удачи в ваших проектах! С удовольствием приму все ваши советы по улучшению данной статьи.
Комментарии (9)
medzumi
06.05.2022 19:06+2Без словоря невозможно будет вызвать метод в кейсе, когда билдится на бэке il2cpp и нужные методы и классы урезаются билдером, т.к. на il2cpp это aot компиляция. Единственный способ защитить от урезания (стрипинга) кода, это link конфигурация ручная.
На il2cpp сейчас билдится iOS (обязательно) и Android (не обязательно), но юнитеки со временем планируют полностью отказаться от Mono (JIT) компилятораWellMOR
07.05.2022 11:58+3Чуть дополню: аттрибут [Preserve].
alowp Автор
07.05.2022 12:43Весьма ценное дополнение! Я создаю ряд инструментов для своих проектов, где данное урезание могло быть критично. Спасибо!
alowp Автор
07.05.2022 12:40Спасибо за пояснение! Я использовал Mono компиляцию, поэтому не знал о поведении консоли при il2cpp компиляции. Рад, что нашёлся человек, способный дополнить статью! ;)
loltrol
08.05.2022 13:44-2А где можно прочитать об отказе от jit? Я точно читал на форуме что есть в идеях отказ от mono в пользу microsoft'овского рантайма для поддержки последних стандартов, но прям что бы от jit... Я тогда unity вообще уважать перестану. Выкиньте уже тогда c# и отдавайте наружу c++ хидеры, а для писателей онлайн казино и три в ряд наверните сверху свой скриптовый язык. Вот зачем насиловать c# своим il2cpp, burst, кривым рантаймом, фиговым пекедж-менеджером...
medzumi
08.05.2022 21:00Про отказ наверное я переборщил (хотя мне кажется я где то находил такое заявление, что они устали фиксить баги под Mono сборку), но продвигают в основном il2cpp. Основной причиной перехода на il2cpp unity была в том что им с проще разрабатывать и фиксить под il2cpp.
red-cat-fat
Поздравляю с первой статьёй - это круто, что ты стремишься поделиться знаниями. Но всё же считаю необходимым сделать несколько замечаний:
Лучше использовать git, нежели облако (это не только делает твою работу удобнее, но и повышает твой авторитет среди разработчиков)
Давать доступ ко всему и сразу - плохое решение, даже для тестирование. Правильным путём будет реализовать
Не стоит заворачивать каждый из пунктов
Перед публикацией стоит ознакомиться с уже существующими решениями, иначе ты можешь просто изобрести никому не нужный велосипед
Если же у тебя не велосипед - стоит обосновать почему он не велосипед и какие преимущества он имеет перед другими решениями
alowp Автор
Благодарю, что не обошли стороной данную статью и не поленились дать советы! Однако, хочу сказать:
1. git у меня на данный момент не работает без vpn. Возможно, его блокирует провайдер...
2. Я стремился к удобству для тех, кто, возможно, хочет использовать это в своих проектах, поэтому и решил выложить все исходники.
3. Мне показалось это удобным... Что ж, в будущем постараюсь не злоупотреблять сворачиванием)
4. Я тщательно ознакомился с существующими решениями перед публикацией, так как искал что-то подобное для своего проекта и не нашёл.
5. Это не велосипед, так как вызова методов без создания словаря я не встречал.