Ребят, столкнулся с какой-то маразматической особенностью C# (на которую у MS конечно же есть объяснения), но моeй инженерной логике она не поддаётся.
Всё очень просто:
class A
{
public int P = 666;
}
class B : A
{
public int P1 = 4;
public int P2;
public B()
{
P = 13;
}
}
// где-то в коде решили создать класс B:var b = new B { P2 = P1 + P };// ошибка прокладки между стулом и монитором!
...и..... ннна тебе поддых канпелятором!! В object initializer ни мембера P, ни P1 канпелятор НЕ ВИДИТ!
Что это за маразм?? Что за такой странный контекст вдруг образовался в элементарном инициализаторе?
Я-то (наивный) думал, что {} — это просто сокращение для b.P2 = b.P1 + b.P ! Похоже, мелкомягкие что-то там намудрили под капотом.
Примером выше я хотел иллюстрировать несколько другой маразм:
Форма из WinForms
Метод()
{
var c = new UserControl {
Left = Width / 2
};
}
Здесь в OI свойство Left КАК И ПОЛОЖЕНО — от UserControl, но Width оказывается берётся от самой формы(!!!). С какого перепоя??
Ведь Width должно браться из самого внутреннего контекста, т.е. от UserControl! Что за бредовая логика заставляет MS лезть в объемлющий контекст?
PS
А я ведь ещё на заре этой дебильной фичи предлагал: не надо этой узколобой привязки инициализатора к конструктору!! Надо, чтобы это работало как with в Паскале:
var j = new J();
// какой-то посторонний код
j init {
.p1 = 3;
.fn();// ДА! И ВЫЗОВ ФУНКЦИЙ ТОЖЕ!!
p2 = .p1;
}
var j2 = new J() init {
.p1 = p2 + 6;
}
Здесь j во-первых может инициализироваться в ЛЮБОМ месте кода, а во-вторых, чётко видно кто и откуда берётся: ".p1" — это внутренний мембер (потому что с точки),
"p2" — мембер из объемлющего контекста. Просто и универсально как лом. Что помешало неумытым "синьорам" с индийских пальм сделать это по-человечески?
Здравствуйте, Baiker, Вы писали:
B>Ребят, столкнулся с какой-то маразматической особенностью C# (на которую у MS конечно же есть объяснения), но моeй инженерной логике она не поддаётся. B>// где-то в коде решили создать класс B:
B>var b = new B { P2 = P1 + P };// ошибка прокладки между стулом и монитором!
B>...и..... ннна тебе поддых канпелятором!! В object initializer ни мембера P, ни P1 канпелятор НЕ ВИДИТ!
Всё верно. Initializer исполняется в том же скоупе, где и new statement. B>Что это за маразм?? Что за такой странный контекст вдруг образовался в элементарном инициализаторе?
Он как раз не образовался. С чего вы ожидали, что он там будет образовываться? B>Я-то (наивный) думал, что {} — это просто сокращение для b.P2 = b.P1 + b.P ! Похоже, мелкомягкие что-то там намудрили под капотом.
Да, совершенно верно — баг в вашей логике. {} — это просто сокращение для b.P2 = P1 + P . Откуда вы там взяли справа префиксы b. — никакой логике не поддаётся.
B>Примером выше я хотел иллюстрировать несколько другой маразм:
B>
B>Форма из WinForms
B> Метод()
B> {
B> var c = new UserControl {
B> Left = Width / 2
B> };
B> }
B>
B>Здесь в OI свойство Left КАК И ПОЛОЖЕНО — от UserControl, но Width оказывается берётся от самой формы(!!!). С какого перепоя??
Эмм. А когда вы пишете руками c.Left = Width / 2, вас не удивляет то, что Width берётся из контекста, а не из c? B>Ведь Width должно браться из самого внутреннего контекста, т.е. от UserControl! Что за бредовая логика заставляет MS лезть в объемлющий контекст?
Нет конечно, с чего вы взяли, что Width должно браться откуда-то ещё, кроме объемлющего контекста?
B>PS B>А я ведь ещё на заре этой дебильной фичи предлагал: не надо этой узколобой привязки инициализатора к конструктору!! B>Здесь j во-первых может инициализироваться в ЛЮБОМ месте кода, а во-вторых, чётко видно кто и откуда берётся: ".p1" — это внутренний мембер (потому что с точки), B> "p2" — мембер из объемлющего контекста. Просто и универсально как лом. Что помешало неумытым "синьорам" с индийских пальм сделать это по-человечески?
Наверное, то, что предлагаемый вами подход создаёт больше проблем, чем решает. В частности, не очень понятно, как быть с вложенными инициализаторами.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
B>>var b = new B { P2 = P1 + P };// ошибка прокладки между стулом и монитором! B>>...и..... ннна тебе поддых канпелятором!! В object initializer ни мембера P, ни P1 канпелятор НЕ ВИДИТ!
S>Всё верно. Initializer исполняется в том же скоупе, где и new statement.
Это "объяснение" никак не объясняет, почему тогда виден P2!
Кроме того, OI — это вам не "скобочки для if" — это специфическая часть, которая работает (у MS) только после new.
Итак, ваше очко переходит зрителям, а вопрос так и остаётся неотвеченным. Перефразирую:
А накой ляд тогда делать вообще OI, если он такой кривой???
Вопрос-уточнение: мне не интересно, что думал Хлипперт и как конкретно он реализовал эту багофичу. С инженерной точки зрения что мешает видеть P1 и P?
B>>Я-то (наивный) думал, что {} — это просто сокращение для b.P2 = b.P1 + b.P ! Похоже, мелкомягкие что-то там намудрили под капотом. S>Да, совершенно верно — баг в вашей логике. {} — это просто сокращение для b.P2 = P1 + P . Откуда вы там взяли справа префиксы b. — никакой логике не поддаётся.
Чушь полная. См. вопрос-уточнение выше.
B>>PS B>>А я ведь ещё на заре этой дебильной фичи предлагал: не надо этой узколобой привязки инициализатора к конструктору!! B>>Здесь j во-первых может инициализироваться в ЛЮБОМ месте кода, а во-вторых, чётко видно кто и откуда берётся: ".p1" — это внутренний мембер (потому что с точки), B>> "p2" — мембер из объемлющего контекста. Просто и универсально как лом. Что помешало неумытым "синьорам" с индийских пальм сделать это по-человечески?
S>Наверное, то, что предлагаемый вами подход создаёт больше проблем, чем решает. В частности, не очень понятно, как быть с вложенными инициализаторами.
Наверное, может надо ПОДУМАТЬ и реализовать? Я сейчас не могу спонталыку просто взять и углубиться в тему компиляторостроения, но если вы приведёте пример такого вот "не очень понятно", то можно поговорить хотя бы о часностях.
Опережающий ответ:
1. Никто не сказал, что можно будет делать вложенные иниты. В конце концов, 99.999999% кода — это как раз простые донельзя конструкции одного уровня.
2. Если они будут вложенные и как-то конфликтовать, ВСЕГДА можно посидеть и придумать какое-то интересное решение.
Главное — БАЗОВАЯ ЛОГИКА должна быть инженерная, а не "у нас тут индусокод и вот так исторически сложилось".
В конце концов, для чего вообще придумывать фичи, кроме как для УДОБСТВА ПРОГРАММИСТА? И на поверку оказалось, что OI-сахарок — чёрте что и полностью обламывает прямую логику разработчика.
Здравствуйте, Sinclair, Вы писали:
B>>PS B>>А я ведь ещё на заре этой дебильной фичи предлагал: не надо этой узколобой привязки инициализатора к конструктору!!
S>Наверное, то, что предлагаемый вами подход создаёт больше проблем, чем решает. В частности, не очень понятно, как быть с вложенными инициализаторами.
По моему конкретному варианту в постскриптуме — вам нравится его реализация? Если нет — давайте обсудим возражения. Если нравится, давайте в рамках предложенного синтаксиса посмотрим на конкретный пример, где "создаёт больше проблем, чем решает". По-моему, как раз мой вариант стократ универсальнее мелкомягкого (напомню — он помогает вообще ВЕЗДЕ в коде, а не только после new).
Вот интересный пример со вложенностью, но я тут "множества нерешённых проблем" не вижу, дополняйте:
int Width = 7;// обычная переменная в outer scopevar btn = new Button init B { // внимание! 'B' - это alias для создаваемого объекта (во внешнем scope это btn)
.Width = Width + 50;// Button.Width = Width + 50 = 57
.Invalidate();// такое MS не сделала даже за 20 лет!
.Height = .Width + 1;// очевидно, что справа от = всё тот же контекст - взяли внутреннего мембера .Width
.Children.Add(niceIcon);
.Styles = new HTMLStyle init { // как и просили, вложенный init
.CSS = "color: #" + .Color;// Button.Color взят извне, т.к. HTMLStyle его не содержит
.File = 1;// error! Нет такого мембера ни у HTMLStyle, ни у Button, внешний scope не рассматривается
.Length = B.Length - 7;// ага! Вот где alias нужен. Вот теперь это HTMLStyle.Length = Button(B).Length - 7
};
};
Здравствуйте, Baiker, Вы писали: B>Это "объяснение" никак не объясняет, почему тогда виден P2!
Он не "виден", он — часть инициализатора.
Вы же заметили, что в object initializer нельзя слева написать произвольное lvalue? А только свойства инициализируемого объекта?
B>Кроме того, OI — это вам не "скобочки для if" — это специфическая часть, которая работает (у MS) только после new.
Всё верно. И она работает предсказуемым, понятным, и устойчивым к мелким изменениям образом.
B>А накой ляд тогда делать вообще OI, если он такой кривой???
Для упрощения инициализации объектов с большим количеством свойств, лишь малая часть которых нужна в каждом конкретном случае. Ваш К.О. B>Вопрос-уточнение: мне не интересно, что думал Хлипперт и как конкретно он реализовал эту багофичу. С инженерной точки зрения что мешает видеть P1 и P?
То, что это принесёт больше вреда, чем пользы.
B>Наверное, может надо ПОДУМАТЬ и реализовать?
Может быть. Пока что видно, что ПОДУМАТЬ не желаете как раз вы.
Я сейчас не могу спонталыку просто взять и углубиться в тему компиляторостроения, но если вы приведёте пример такого вот "не очень понятно", то можно поговорить хотя бы о часностях. B>Опережающий ответ: B>1. Никто не сказал, что можно будет делать вложенные иниты.
И как вы собираетесь их запретить? На всякий случай поясню, что Object Initializer — это expression. И его, естественно, можно поставить в любое место, где нужен expression.
То есть примерно везде — включая объемлющий object initializer. B>В конце концов, 99.999999% кода — это как раз простые донельзя конструкции одного уровня.
Основная ценность object initializer — как раз конструирование сложных деревьев объектов. Если у вас простая линейная конструкция, то вам не нужен никакой object initializer — просто напишите цепочку присваиваний свойств.
B>2. Если они будут вложенные и как-то конфликтовать, ВСЕГДА можно посидеть и придумать какое-то интересное решение.
Проблема не в том, чтобы придумать решение. А в том, чтобы это решение не приводило к трудноуловимым багам и WTF. В том числе и в corner cases.
Вот вам простой пример. Версия кода 1:
public class Foo
{
public string DisplayName {get;set;}
public int Number {get; init;}
public Foo(int number)
{
Number = number;
DisplayName = number.ToString();
}
}
public class Bar
{
public string Name {get; init;}
public List<Foo> Foos {get; } = new();
public Bar(int seed) => Name = $"Bar #{new Random(seed).Next()}";
}
public class Program
{
public static void Main()
{
var b = new Bar(42) {
Foos = {
new Foo(1) { DisplayName = Name + ":" + Number},
new Foo(2) { DisplayName = Name + ":" + Number}
}
};
}
}
вроде всё хорошо, да? Name берётся из "текущего" Bar, Number — из "текущего" Foo.
Теперь автор класса Foo, который ничего не знает о коде Main (и, может быть, даже о коде Bar) публикует версию 2, добавляя невинно выглядящее свойство:
public class Foo
{
public string Name {get;set;}public string DisplayName {get;set;}
public int Number {get; init;}
public Foo(int number)
{
Number = number;
Name = number.ToString();
DisplayName = number.ToString();
}
}
Автор Main забирает апдейт, компилирует. Всё успешно. Когда он получит баг репорт, он долго пытается понять, почему хорошо отлаженный код вдруг стал выдавать какую-то ересь.
Но вопрос даже не в этом — ну, допустим, он понял, в чём дело. Что ему теперь делать? Как вы предлагаете ему писать код Main так, чтобы он работал как в предыдущем случае?
B>Главное — БАЗОВАЯ ЛОГИКА должна быть инженерная, а не "у нас тут индусокод и вот так исторически сложилось".
Совершенно верно. Базовая логика как раз инженерная.
B>В конце концов, для чего вообще придумывать фичи, кроме как для УДОБСТВА ПРОГРАММИСТА? И на поверку оказалось, что OI-сахарок — чёрте что и полностью обламывает прямую логику разработчика.
Обламывает он только лично вас — и то потому, что вы вместо изучения инструмента придумываете неудачные варианты того, как он мог бы (а точнее — не мог бы) работать.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
B>>2. Если они будут вложенные и как-то конфликтовать, ВСЕГДА можно посидеть и придумать какое-то интересное решение. S>Проблема не в том, чтобы придумать решение. А в том, чтобы это решение не приводило к трудноуловимым багам и WTF. В том числе и в corner cases.
Все подобные вещи надо обдумывать, тестировать, проверять на существующей кодовой базе — НИЧЕГО сложного или нереализуемого.
И по факту, "пришлёпка" сахара в виде мелкомягкой OI — это самая тупая и ограниченная реализация того, что могло бы быть.
S> new Foo(1) { DisplayName = Name + ":" + Number}, S>вроде всё хорошо, да? Name берётся из "текущего" Bar, Number — из "текущего" Foo. S>Теперь автор класса Foo, который ничего не знает о коде Main (и, может быть, даже о коде Bar) публикует версию 2, добавляя невинно выглядящее свойство: S> public string Name {get;set;}
S>Но вопрос даже не в этом — ну, допустим, он понял, в чём дело. Что ему теперь делать? Как вы предлагаете ему писать код Main так, чтобы он работал как в предыдущем случае?
Здравствуйте, Baiker, Вы писали:
B>По моему конкретному варианту в постскриптуме — вам нравится его реализация?
Нет, не нравится.
B>Если нет — давайте обсудим возражения.
B>Вот интересный пример со вложенностью, но я тут "множества нерешённых проблем" не вижу, дополняйте:
B>
B>int Width = 7;// обычная переменная в outer scope
B>var btn = new Button init B { // внимание! 'B' - это alias для создаваемого объекта (во внешнем scope это btn)
B> .Width = Width + 50;// Button.Width = Width + 50 = 57
B> .Invalidate();// такое MS не сделала даже за 20 лет!
B> .Height = .Width + 1;// очевидно, что справа от = всё тот же контекст - взяли внутреннего мембера .Width
B> .Children.Add(niceIcon);
B> .Styles = new HTMLStyle init { // как и просили, вложенный init
B> .CSS = "color: #" + .Color;// Button.Color взят извне, т.к. HTMLStyle его не содержит
B> .File = 1;// error! Нет такого мембера ни у HTMLStyle, ни у Button, внешний scope не рассматривается
B> .Length = B.Length - 7;// ага! Вот где alias нужен. Вот теперь это HTMLStyle.Length = Button(B).Length - 7
B> };
B>};
B>
Вот видите — первый же заданный вопрос заставил вас менять синтаксис. Теперь в init появился опциональный алиас. Причём теперь нужно не забыть его указать, иначе получится бяка.
Двигаемся дальше. Вот у вас в .Styles не было .Color, а вот он появился. Всё, поведение кода поменялось, без малейших подсказок со стороны компилятора. Удачной отладки.
Коммент про .File — а что, если бы у Button было такое свойство, то всё бы скомпилировалось?
Двигаемся дальше. В чём у нас семантика этого init? Слева от него — некое object expression, справа — опциональный алиас плюс некий блок кода.
Что это за блок кода такой? А это такой блок кода, где (это я угадываю телепатически)
1. Определён указанный выше алиас, если он есть
2. Разрешены только операторы присваивания и вызовы методов (причём не любых, см далее)
3. В аргументах методов и в выражениях операторов присваивания разрешены обращения к свойствам и методам через точку без object expression слева.
Для начала нужно убедиться, что у нас нет проблем с п.3. Не будет ли конфликтов при разборе? Я пока вижу один нехороший сценарий: взаимодействие с тернарным оператором и null-checking member access. Выражения типа x > y ? .Inc() похожи и на x > (y?.Inc()), и на (x > y)?.Inc()). А теперь у нас ещё и добавляется прекрасная возможность (x > y) ? (.Inc()), то есть "Syntax error, : expected".
Когда мы с этим разберёмся, захочется понять, какие именно методы можно вызывать, а какие — нет. Способ поиска методов слева от =, справа от =, и просто при записи .Invalidate() будет отличаться? Я так понимаю, что для "справа от = " вы хотите, чтобы выполнялся поиск вверх во всех init-блоках, пока не найдется подходящий метод. Будет ли это работать также без =? То есть если я пишу btn init{.Styles init {.Invalidate()}}; — будет ли успешно вызываться btn.Invalidate()?
Рассматриваются ли extension-методы?
Самое интересное тут вот в чём: как будем разруливать перегрузки методов? На всякий случай: в С# итак уже весьма навороченные (и местами неожиданные) правила байндинга вызовов (см. пример). Теперь вы предлагаете добавить к этой кунсткамере ещё одно измерение: подбор подходящего this в иерархии инициализируемых объектов. Как вы себе это видите — хотя бы в общих чертах? Вот у нас завелись два похожих метода: Button.Foo(int i), HtmlStyle.Foo(decimal d). Мы наблюдаем такое выражение: btn init{.Styles init {.Length = .Foo(42)}};. Какой из Foo будет вызван? Можно позвать .Styles.Foo — 42 implicitly convertible to decimal, и он доступен во внутреннем scope. Но, может быть, надо звать btn.Foo — ведь он better fit, т.к. там вообще не надо преобразовывать параметры? Или лучше всё же падать с CS0121: The call is ambiguous? Примерно к любому варианту, который вы выберете, я напишу контрпример, который работает "не так, как интуитивно ожидается".
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Baiker, Вы писали:
B>>
B>>int Width = 7;// обычная переменная в outer scope
B>>var btn = new Button init B { // внимание! 'B' - это alias для создаваемого объекта (во внешнем scope это btn)
B>> .Width = Width + 50;// Button.Width = Width + 50 = 57
B>> .Invalidate();// такое MS не сделала даже за 20 лет!
B>> .Height = .Width + 1;// очевидно, что справа от = всё тот же контекст - взяли внутреннего мембера .Width
B>> .Children.Add(niceIcon);
B>> .Styles = new HTMLStyle init { // как и просили, вложенный init
B>> .CSS = "color: #" + .Color;// Button.Color взят извне, т.к. HTMLStyle его не содержит
B>> .File = 1;// error! Нет такого мембера ни у HTMLStyle, ни у Button, внешний scope не рассматривается
B>> .Length = B.Length - 7;// ага! Вот где alias нужен. Вот теперь это HTMLStyle.Length = Button(B).Length - 7
B>> };
B>>};
B>>
S>Вот видите — первый же заданный вопрос заставил вас менять синтаксис
И что в этом такого?? Я что, принёс вам готовые спеки что ли?? Глупый наезд на почве личной опупительности. НЕ НАДО на меня наезжать! Я ни вам, ни микрософту ничего не должен.
Изменил синтаксис — значит так надо, значит СТАЛО ЛУЧШЕ. Так или иначе, я предлагаю улучшать компилятор, а не бодаться с луддитами и "исторически сложилось" факапами.
S> Теперь в init появился опциональный алиас. Причём теперь нужно не забыть его указать, иначе получится бяка.
Разумеется указать! Если он нужен. А на его нужность укажет компилятор в местах с неоднозначностями. В ЧЁМ ПРОБЛЕМА-ТО?? Я не понимаю этого агрессивного противостояния — нужно ПОМОГАТЬ МНЕ решить эту синтаксическую проблему (если она вообще есть), а не выделываться здесь "мы в микрософт все умные, а вы ничего не понимаете!".
S>Двигаемся дальше. Вот у вас в .Styles не было .Color, а вот он появился. Всё, поведение кода поменялось, без малейших подсказок со стороны компилятора. Удачной отладки.
Да, это проблема. Но не такая проблема, чтобы прям осбосраться и отменить фичу! Просто надо чутка подумать и решить, как ОБЯЗАТЕЛЬНО реализовать фичу, но предусмотреть этот сложный случай.
К примеру, сам компилятор проверит, что было использование одного объекта, а после введения члена появился другой. Ни бог весть какая проблема — БЫЛО БЫ ЖЕЛАНИЕ РЕШИТЬ.
S>Коммент про .File — а что, если бы у Button было такое свойство, то всё бы скомпилировалось?
Да. В чём проблема?
S>Двигаемся дальше. В чём у нас семантика этого init?
С самого начала поста объяснил, так сложно доходит? СОКРАЩЕНИЕ КОДА.
S>1. Определён указанный выше алиас, если он есть S>2. Разрешены только операторы присваивания и вызовы методов (причём не любых, см далее) S>3. В аргументах методов и в выражениях операторов присваивания разрешены обращения к свойствам и методам через точку без object expression слева.
Да, всё верно.
S>Для начала нужно убедиться, что у нас нет проблем с п.3. Не будет ли конфликтов при разборе? Я пока вижу один нехороший сценарий: взаимодействие с тернарным оператором и null-checking member access. Выражения типа x > y ? .Inc() похожи и на x > (y?.Inc()), и на (x > y)?.Inc()). А теперь у нас ещё и добавляется прекрасная возможность (x > y) ? (.Inc()), то есть "Syntax error, : expected".
Решается обычным приоритетом операторов! На крайняк, НЕТ такого оператора "?␣." — есть "?.". Пробел значим. В любом случае, ЕСТЬ СКОБКИ и подсветка/подсказка. Навёл курсор на .Inc() — увидел, от кого взят мембер.
S>Способ поиска методов слева от =, справа от =, и просто при записи .Invalidate() будет отличаться?
Нет, он должен быть одинаков (как и ожидает программист, просто СОКРАЩАЯ КОД).
S> Я так понимаю, что для "справа от = " вы хотите, чтобы выполнялся поиск вверх во всех init-блоках, пока не найдется подходящий метод. Будет ли это работать также без =? То есть если я пишу btn init{.Styles init {.Invalidate()}}; — будет ли успешно вызываться btn.Invalidate()?
Разумеется! Я же привёл пример:
B>> .Invalidate();// такое MS не сделала даже за 20 лет!
Здесь нет никакого присвоения, просто ищем подходящий объект.
S>Рассматриваются ли extension-методы?
Тут я ещё не думал, но... почему бы и нет? У компилятора ВСЕГДА есть шанс сказать, когда у него неоднозначность! Скажем, если будет код:
var lst1 = new List<File>() init {
var lst2 = new List<File>() init {
.Sort();// чей сорт?
}
}
...выправляем его так:
var lst1 = new List<File>() init L {
var lst2 = new List<File>() init {
L.Sort();// ага, знаю чей!
}
}
S>Самое интересное тут вот в чём: как будем разруливать перегрузки методов? Вот у нас завелись два похожих метода: Button.Foo(int i), HtmlStyle.Foo(decimal d). Мы наблюдаем такое выражение: btn init{.Styles init {.Length = .Foo(42)}};. Какой из Foo будет вызван?
Согласен, тонкий И РЕДКИЙ случай — выкинем ambiguous. Что сложного-то?? Не надо пытаться сделать больше, чем позволено "тупой машине"! (иначе сделаешь только хуже)
Тут принцип построения фич простой: запиливаем фичу для большинства случаев (предварительно хорошенько рассмотрев все случаи). Если люди дадут фидбэк "вот у нас такой часто используемый код, а он лажает на вашем синтаксисе" — повторяем SDLC и допиливаем как надо (фича всё равно в АЛЬФА ТЕСТЕ!). И так до полной шлифовки.
S>Примерно к любому варианту, который вы выберете, я напишу контрпример, который работает "не так, как интуитивно ожидается".
Да ради бога! Чем больше контр-примеров, тем шире почва для обсуждения (а не отмазок НЕ ДЕЛАТЬ). Факт тот, что фичу МОЖНО и НУЖНО сделать. Я не глубокий спец именно в компиляторах, но я знаю, что если что-то делать — то оно сделается! А если в стиле Хлипперта придумывать "почему мы НЕ ХОТИМ это делать" — то ничего и не будет.
Пока что из обсуждения я не увидел НИ ЕДИНОГО аргумента "эту фичу принципиально нельзя сделать" — ВСЁ ВОЗМОЖНО, но постепенно. А вы сейчас своей ДЕМАГОГИЕЙ пытаетесь грубо говоря "закидать шапками". Так люди НЕ КООПЕРИРУТСЯ! Мыслить надо ДРУЖЕСКИ, а не сидеть как тухлый ёж и совать всем иголки.