Pull to refresh

Comments 159

Разработка новой версии языка определенно не идет за закрытыми дверями…

Так в чем вопрос, надо просто открыть двери
Лично я бы проголосовал за

1 Множественное наследование.
2 virtual static function
Эти фичи требуют поддержки не только компилятора, но и самого рантайма.

P.S. Я лично против множественного наследования в том виде, как это сделано в C++. Если делать что-то подобное, то лучше реализовать систему mixin'ов.
Нет. Множественное наследование с успехом заменяется интерфейсами, но при этом не привносит связанных с множественным наследованием проблем. А понятия виртуальной статической функции не существует в природе.
>А понятия виртуальной статической функции не существует в природе.

Как же не существует, если мы ее обсуждаем?

И не вижу причин, по которым невозможно, это реализовать в C#. (это конечно не значит, что таких причин не существует)

UFO just landed and posted this here
Понятие «виртуальная статическая функция», по моему интуитивно понятно, это статическая функция которую можно наследовать.
Поясните на примере, мне очень интересно, что вы имеете в виду. Поначалу я подумал, что вы имели в виду статический полиморфизм (шаблоны C++), но слово «static» однозначно даёт понять, что вы имеете в виду что-то непонятное.

Слово «виртуальная» означает, что:
— функция может быть переопределена в производных классах.
— конкретная реализация функции определяется в момент исполнения программы в зависимости от объекта, у которого она вызывается.

Вы можете наследовать статические классы и вызывать в них функции базовых классов — никто этого не запрещает. Вы можете сокрыть функцию с той же сигнатурой. Но вы не можете сделать статическую функцию виртуальной, потому что нет объекта, который бы отвечал за выбор конкретной реализации функции.

Наверное, как объектно-ориентированных языках, класс, это тоже такой объект.


типа


abstract class Animal{
    abstract virtual static string GetDescription();
}
class Dog{
    static string GetDescription()
    {
        return "собака";
      }
}

var animalClasses = new []{Dog, Cat, Squirell};
Animal CreateByDescription(string description)
{
     return animalClasses
          .Where(x => x.GetDescription() == descriuption)
          .First()
          .CreateNewInstance();
}
UFO just landed and posted this here

Речь идет об изменении языка => можно ввести такое

Для этого вам придется не только язык поменять, но и солидный кусок CLR.

Не факт. Можно какой-нибудь трюк типа автобоксинга сделать. При приведении к типу MetaClass генерировать какой-нибудь класс с членами статическими методами.

… которые хранить где?

Не знаю. Может быть в сборке с классом. Тогда, наверное, надо его генерить по атрибуту а не при приведении. Вы лучше меня знаете дотнет, придумайте сами :)

Я просто не могу придумать никакого сценария, при котором я обращаюсь именно к статическому методу в терминах CLR (т.е. <typename>.<method>) и при этом у меня работает subclass polymorphism.


Есть дженерики, конечно, но там на момент развертывания известен точный тип.

SuperType.classType t = SubType.class;
t.StaticVirtualMethod();

Мне такая идея тоже не нравится — но это не значит что она нереализуема.

AnimalFactory[] factories = new[]{ new CatFactory(), new DogFactory(), new SquirellFactory(), };
Animal CreateByDescription(string description)
{
     return factories 
          .Where(x => x.Description == description)
          .First()
          .Create();
}
Статические виртуальные методы можно имитировать с помощью фабрик.
Как бонус мы получаем состояние, (почти) не используя статических полей. Также возможность иметь несколько настраиваемых фабрик и передавать их параметром. И отдельную фабрику для тестов.

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

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

Не соглашусь. Знаю пару случаев использования C# как скриптового языка (с помощью Roslyn). И рефлексия там запрещена из соображений безопасности.
Ну, кстати, статические методы в интерфейсах были бы крайне полезны. Как пример использования — десериализация.

interface IDeserealizable
{
static IDeserealizable deserialize(IDeserializer deserializer)
}

А ещё:
— указание нескольких интерфейсов в качестве типа поля
— реализация интерфейсов через расширение

Собственно, это фичи Objective-C.
Как, собственно, и «виртуальные статические функции». В том плане, что статические функции в ObjC перегружать можно.

В случае с C# — не знаю, никогда не пытался :) Но сама затея в C# на данный момент выглядит безнадёжной по той причине, что для статических методов почти нет фич, где бы их можно было использовать.
>Но вы не можете сделать статическую функцию виртуальной, потому что нет объекта, который бы отвечал за выбор конкретной реализации функции.

Для статической функции объект совсем не обязателен, вполне хватит и самого класса.

Похоже, что действительно возникло недопонимание в определении, что такое virtual static, даю пример

class A
{
public virtual static string info() {return «a»;}
}

class A2:A
{
public virtual static string info() {return «a2»;}
}
А зачем?
Вне зависимости от того, что именно вы хотите сделать — это можно сделать через сокрытие (new на функции)

class A {
   public static string info() {return "a";}
}

class A2:A {
    public new static string info() {return "a2";}
}


С точки зрения языка, виртуальная статическая функция — бессмысленна, виртуальные методы нужны для маршрутизации с вызовом типа, который на самом деле лежит под базовым типом/интерфесом.
При вызове статической функции — тип строго определен, маршрутизация типов не нужна.
Понял, почему народ так удивлялся — C# просто не позволяет вызвать через экземпляра класса статическую функцию этого класса.
Тогда естественно виртуальность статик функции теряет смысл.

Так что появилось еще одно пожелание:
Возможность вызова статик функции через экземпляра класса.
Тогда чем статическая функция будет отличаться от обычной?

Сейчас модификатор static — принадлежность функции типу целиком, а не экземпляру.

Она не должна знать ничего о «нестатических» полях/методах/св-вах.
Я чуть о другом, вот такой код не работает:

class A {
public static string info() {return «a»;}
}

A a = new A();
string s = a.info(); < — тут баг
Здесь вы просто ходите, чтобы в таблице виртуальных функций объекта существовали функции, которые не принимают this в качестве первого аргумента, например, из соображений вычислительных затрат. Но при этом эти функции можно вызывать и как статические — при явном указании типа компилятор просто подставит нужную реализацию.

Идея выглядит разумной. Но при этом не рассматривается вопрос вызова статической функции не по объекту, а по типу (generic).
UFO just landed and posted this here
>Но при этом не рассматривается вопрос вызова статической функции не по объекту, а по типу (generic).

Почему же?
Для A a; одинаково должно вызываться и

A.info();
и
a.info();
Это почему же?

A a = new B();

A.info() — вызов статического метода класса A
a.info() — вполне может быть вызовом метода info класса B
Конечно! В этом весь смысл.

В прочем я нашел способ решить эту проблему с помощью reflection, но решение мне не нравится.

Хочется возможностей самого языка, тем более, что новый синтаксис для этого не нужен.
А явных проблем, почему это нельзя сделать, лично я не вижу.
Можно же просто добавить обычный виртуальный метод объекту, и из него вызвать статический метод.

Будет два метода: статический, который можно вызвать по имени класса, и аналогичный ему виртуальный, который можно вызвать через экземпляр. Оверхед на передачу ненужного параметра this в любом случае ниже, чем затраты на рефлексию.
Возможность вызова статик функции через экземпляра класса.

Но зачем?

Как минимум, наличие возможности лучшее ее отсутствия. Тем более возможности которая никак не изменяет ни синтаксис, не идеологию.

А так это дает более объектно ориентированный доступ к параметрам и функциям которые меняются при наследовании, но не нуждаются для расчетов в самом объекте.

Это хорошо проявляется в задачах типа фабрика объектов.
Как минимум, наличие возможности лучшее ее отсутствия.

Нет. Слишком много возможностей нарушает целостность.


А так это дает более объектно ориентированный доступ к параметрам и функциям которые меняются при наследовании, но не нуждаются для расчетов в самом объекте.

Если они не нуждаются для расчетов в объекте — это не функции этого объекта. И именно поэтому синтаксис объект.метод по отношению к ним будет вводить в заблуждение.

>Нет. Слишком много возможностей нарушает целостность.

Я так понимаю вы поклонник асемблера и противник библиотек и объектного программирования?

> Если они не нуждаются для расчетов в объекте — это не функции этого объекта.

Конечно — это функция класса. Но для того, что бы она вызвалась у объекта именно того класса которым он является, ее нужно вызывать непосредственно у этого экземпляра класса. Пояснить примером?
Я так понимаю вы поклонник асемблера и противник библиотек и объектного программирования?

Нет, я поклонник консистентных систем и противник избыточности.


Но для того, что бы она вызвалась у объекта именно того класса которым он является, ее нужно вызывать непосредственно у этого экземпляра класса.

Нет такого "нужно". Если вам нужна функция класса, вам нужно вызывать ее на классе. Типичный пример такого (на псевдоязычке) выглядел бы так:


yourObject -> getType -> someMethod
Ну а я за предоставление максимальных возможностей. Только не надо писать про выстрелы в ногу, это не тот случай.

>yourObject -> getType -> someMethod

Именно! Но зачем «getType ->» если его нужно писать всегда? Можно и пропустить двузначностей это не принесет.
Тем более «нормального» getType к сожалению нет :( и вроде делать его не собираются.

Или вас возмущает, что вызов функций у объекта и через ссылку на объект идет через "."?

Тогда да, тогда я понимаю вашу принципиальность.
Но зачем «getType ->» если его нужно писать всегда?

Не всегда, а только тогда, когда обращение идет к методам класса, а не объекта. И именно за этим и нужно — чтобы четко видеть, к какому объекту мы обращаемся.


Можно и пропустить двузначностей это не принесет.

Да ну? yourObject -> Name — имя чего я сейчас получу? yourObject -> setMaxCount(15) — у чего ограничился пул?


Тем более «нормального» getType к сожалению нет

Так вот лучше сделать нормальный getType, который не нарушает парадигму, чем вводить неявный вызов статиков, который парадигму нарушает.

>Да ну? yourObject -> Name — имя чего я сейчас получу?

Что функция вернет то и будет. Я имел в виду неоднозначностей для компилятора.

>Так вот лучше сделать нормальный getType

Это бесспорно «нормальный getType» без рефлектинга по мне тоже куда важнее. Хотя и проблем вызовом статиков я не вижу.

Похоже я больше склонен к универсальности и общему синтаксису, а вы за строгость и явные разграничения.
вы за строгость и явные разграничения.

И это правильно. Чтобы стрелять в ноги, у нас уже есть C++, оставьте C# в покое.
>И это правильно. Чтобы стрелять в ноги, у нас уже есть C++, оставьте C# в покое.

:) Расскажите мистер паладин, чем именно ужасен вызов статика из объекта?
Например тем, что объект может оказаться null. Ловить NullReferenceException на статических методах — не очень большая радость. И вообще тогда какие отличия от экземплярных методов, кроме потери доступа к this в теле метода?
:) Вызов любой функции может дать NullReferenceException.
А самое забавное, что вызов статической функции НИКАК не может вызвать NullReferenceException.

>И вообще тогда какие отличия от экземплярных методов, кроме потери доступа к this в теле метода?

В том что их можно вызвать и без наличия экземпляра класса.
>> Вызов любой функции может дать NullReferenceException.
>> А самое забавное, что вызов статической функции НИКАК не может вызвать NullReferenceException.
Вы противоречите сами себе.

class A { public static virtual info() {...}; }
class B: A { public static override info() {...}; }

A a = null;
a.info(); // что произойдет по-вашему?
Отработает функция, она же обращаться к полям не будет.
Какая? Класса A или класса B?
Если речь о том, чтобы сделать две одинаково работающих возможности вызова статик метода: A.info() и a.info(), то действительно работать будет. Только непонятно, зачем.
Но о виртуальных статических методах можно забыть из-за возможного null.
>Какая? Класса A или класса B?

Конечно класса А.
А если (B(null)).info(); то от B; Что вас смущает?

>Но о виртуальных статических методах можно забыть из-за возможного null.

Откуда null, в статик функции? Даже у виртуальной.
>> А если (B(null)).info(); то от B; Что вас смущает?
Здесь — ничего. А если так:
A a = new B();
a.info();

Должен вызваться метод A.info() или B.info()?

>> Откуда null, в статик функции? Даже у виртуальной.
А как CLR должен в рантайме определить, виртуальную статическую функцию какого класса вызывать? Видимо, по экземпляру класса. Если он null, все плохо. Если не по экземпляру, то как еще?
>А если так: A a = new B(); a.info();

B.info() или есть сомнения?

>Если он null, все плохо

Все хорошо — тип объекта то есть. Вот по типу и вызовет.

>> Все хорошо — тип объекта то есть. Вот по типу и вызовет.
Вот в этом и проблема. Типа объекта нет.
1. Во время компиляции конкретный тип объекта неизвестен.
2. Во время исполнения у вас, грубо говоря, в памяти 0 (т.е. null). Метаданные связываются с конкретным экземпляром класса, а не со ссылкой на него. Соответственно, по null ссылке вы никак не узнаете, на экземпляр какого типа она указывает.
Скорее всего у вас нет специфических знаний по вызовам виртуальных функций в рантайме.
Поэтому давайте решим так, если бы вы реализовывали вызовы виртуальных функций вы бы не смогли сделать вызов функций у null объектов.
Скорее всего у вас нет специфических знаний по вызовам виртуальных функций в рантайме.

Так просветите, интересно же.
Мне вот кажется, что это вы не знаете, как CLR хранит ссылки на объекты и сами объекты. Если бы со ссылкой связывалась информация о типе объекта, тогда да, проблем нет. Но информации о типе у ссылки нет.

Значит будет NullReferenceException, как и у обычных функций.

О чем и речь. Мы получили экземплярный виртуальный метод. Зачем тогда делать статический метод виртуальным, чтобы он вел себя ровно так же, как экземплярный? Причем это полное нарушение концепции статических методов.

В этом и есть смысл — единообразие вызовов статик методов и экземпляр методов. С возможностью вызова base метода.

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


Можно вести речь о том, чтобы заиметь в языке что-то вроде "класса как объекта", и писать, например:


class A { static virtual void foo() {...}}
class B : A { static override void foo() {...}}

Class<A> a = A;
a.foo(); // вызывает A.foo()

a = B;
a.foo(); // вызывает B.foo()

a = (new B())->class;
a.foo(); // вызывает B.foo()

Но вызывать статику на экземплярах — увольте.

Хорошо, например есть набор классов A1 A2 А3 и т.д. порожденные от А.
у них общий набор типовых характеристик например
static Name, Type, Width, Height, у каждого класса свои значения. И возможно они зависит от расчета в родителе.

Эти данные используются как без объектов, так и внутри объектов.
Как удобней писать? в месте каждого вызова A1.Width() или просто Width()?
И сколько будет ошибок при copy/paste.

А если бы у этих статик функций было наследование, то можно было бы общую часть их обработки вообще отдать родителю:

Например классу А реализовать функцию
static int Perimetr() {return (Width() + Height())*2;}
Которая нормально работала бы для все наследников.
у них общий набор типовых характеристик например
static Name, Type, Width, Height, у каждого класса свои значения. И возможно они зависит от расчета в родителе.

Почему бы не сделать какое-нибудь статическое свойство Meta, куда в статическом конструкторе положить экземпляр класса характеристик с нужными значениями, привязкой к базовым характеристикам и т.п.? Будет у вас A1.Meta.Width, например. Реализация чуть сложнее, но снаружи разницы особо нет. Можно добавить еще экземплярное свойство Meta, которое просто возвращает значение статического. Тогда и на экземплярах удобно будет звать статику.


Как удобней писать? в месте каждого вызова A1.Width() или просто Width()?

По мне, удобнее писать одни и те же вещи одинаково, и разные вещи по-разному. Именно поэтому для статики писать везде A1.Width лучше, чем где-то A1.Width, а где-то a.Width (который статический, но пишется почему-то как экземплярный).

Как-то сомнительно звучит, думаю не получится.
class A
{
public static string inf;
}

class A2: A
{
}

A.inf = «a»;
A2.inf = «a2»;

после такого в A.inf будет «a2».

Или я не так понял идею?

Немного не так:


class A {   public static string inf; }
class A2 : A {  public static new string inf; }

A.inf = "a";
A2.inf = "a2";

Хотя решение не очень красивое, конечно.


Если в более общем виде, то так
using System;

class Meta
{
    string _meta;
    public Meta(string meta)
    {
        _meta = meta;
    }

    public override string ToString()
    {
        return _meta;
    }
}

class A
{
    public static Meta meta;
}

class A1 : A
{
    public static new Meta meta;
}

class A2 : A
{
    public static new Meta meta;
}

public class Program
{
    public static void Main()
    {
        A1.meta = new Meta("a1");
        A2.meta = new Meta("a2");
        Console.WriteLine(A.meta);
        Console.WriteLine(A1.meta);
        Console.WriteLine(A2.meta);
    }
}

Может, стоило бы вообще убрать такого рода информацию из статических полей и организовать доступ к ней как-то еще. Например, importantInfoProvider.Get().Width. А внутри по типу определять, что вернуть. Не думаю, что решение через статику — единственное или лучшее возможное.

Пожалуйста:

interface IDerivedStatic
            {
                int Width();
                int Height();
            }

            class A<T>
                where T : struct, IDerivedStatic
            {
                static T data = new T();

                public static int Width() => data.Width();
                public static int Height() => data.Height();
                public static int Perimetr() => Width() * Height();
            }

            class A1
                : A<A1.Static>
            {
                public struct Static
                    : IDerivedStatic
                {
                    public int Height() => A1.Height();
                    public int Width() => A1.Width();
                }

                static new int Width() => 12;
                static new int Height() => 14;
            }


Использование struct заставляет JIT генерировать отдельный для каждого типа, а не вызывать интерфейс + нет оверхеда по памяти.

Делал замеры — всё инлайнится, производительность идентична прямому вызовы.
Биг сенскс!

Хоть и выглядит куда более сложно чем через virtual / override. Но похоже это решает проблему переопределения статик значений.
Да-да, именно так. При взгляде на код должно быть чётко понятно, где вызывается метод объекта, а где — статическая функция. Именно поэтому нельзя смешивать этот синтаксис.

Если над идеей статических виртуальных функции хорошо подумать, то может, что-то годное и выйдет. Одной из возможных реализаций этого механизма была бы следующая: GetType() возвращает не Type, а унаследованный от Type объект, содержащий вызовы статических методов. А для упрощения синтаксиса можно добавить ключевое слово какое-нибудь (a.class — см.выше).

То есть

class A
{
    static virtual void foo() { ... }
}


разворачивается в

class A.Static: Type
{
    virtual void foo() { A.foo(); } }
}

class A
{
    static void foo() { ... }
    A.Static GetType();
}


И используется так, например:

void Generic<T>(T a) where T: A
{
    a.GetType().foo();
}

void Generic<T>() where T: A
{
    // typeof(T) возвращает объект T.Static, приведённый к A.Static
    // это можно сделать точно так же, как реализован механизм new T()
    typeof(T).foo();
}


В таком виде я был бы даже не против увидеть это в языке.
Еще раз спасибо за пример, натолкнул на некоторые идеи.

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

class A
{
public virtual static int Width() => 10;
public virtual static int Height() => 11;

public static int Square() => Width() * Height();
}

class A1: A
{
pubilc override static int Width() => 12;
pubilc override static int Height() => 14;
}

По моему проще.
И вызовы были единообразные
A1.Square();
и
A a1 = new A1();
a1.Square();

вообще красота (по мне) :)|
Теоретически какой-то смысл в этом есть: вызывать виртуальный метод, не передавая указатель на объект — меньше на 1 параметр и можно вызвать метод у пустого объекта, передавая голый vfptr, типа A.class.init().

Практически же передача одного параметра (ещё и через регистр) — ничто по сравнению с виртуальным вызовом (переходом по адресу), а язык и CLR усложнит.
Что функция вернет то и будет. Я имел в виду неоднозначностей для компилятора.

Меня мало волнует компилятор, меня волнует, как я это читать буду. И вот мне это читать очень неудобно.


Это бесспорно «нормальный getType» без рефлектинга по мне тоже куда важнее.

Вообще-то GetType и сейчас не использует Reflection.

На вкус и цвет фломастеры разные.

Но для того что бы использовать Type для вызова функций нужен Reflection.
Но для того что бы использовать Type для вызова функций нужен Reflection.

Не так. Использование метаданных для (например) позднего вызова функций — это и есть reflection, как подсистема CLR.

А как можно наследовать функцию, подскажите?
UFO just landed and posted this here
Спасибо! Вместе с примером с IDeserealizable теперь действительно понятно, что это такое и как может применяться. Единственная область применения этого механизма — вызов статических методов для generic типов. Сейчас же такая возможность предусмотрена только для конструкторов (new()).

Реализация статических интерфейсов потребует довольно серьёзное изменение CLR, на что MS пойти не сможет точно.

Прямо сейчас этот функционал можно сэмулировать путём создания дженерик класса со статическим полем — делегатом на статический метод, который создаётся единожды через рефлексию в статическом конструкторе, что-то вроде:

struct Deserializer<T> where T: IDeserializable
{
    public static readonly Func<T> deserialize(IDeserializer deserializer);

    static Deserializer()
    {
        deserialize = что-то типа typeof(T).GetMethod("deserialize", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(Func<T>)) as Func<T>;
    }
}

и вызывать как
Deserializer<T>.Deserialize(...);
> Единственная область применения этого механизма — вызов статических методов для generic типов.
Generic-типов и интерфейсов

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

И тогда статические методы становятся мощным инструментом для написания фабрик с простым синтаксисом из без магии рантайма внутри.
Те кто работал с Delphi знают и применяют так или иначе виртуальные конструкторы, статические виртуальные методы (class virtual) и конструкции языка типа
TVirtClass = class of TBaseVirtClass.

В Delphi метакласс это ссылка на VMT и все виртуальные методы класса (не экземпляра класса) располагаются с отрицательным смещением
И все метаклассы наследуются от
TClass= class of TObject.
{ Virtual method table entries }

vmtSelfPtr = -76;
vmtIntfTable = -72;
vmtAutoTable = -68;
vmtInitTable = -64;
vmtTypeInfo = -60;
vmtFieldTable = -56;
vmtMethodTable = -52;
vmtDynamicTable = -48;
vmtClassName = -44;
vmtInstanceSize = -40;
vmtParent = -36;
vmtSafeCallException = -32 deprecated; // don't use these constants.
vmtAfterConstruction = -28 deprecated; // use VMTOFFSET in asm code instead
vmtBeforeDestruction = -24 deprecated;
vmtDispatch = -20 deprecated;
vmtDefaultHandler = -16 deprecated;
vmtNewInstance = -12 deprecated;
vmtFreeInstance = -8 deprecated;
vmtDestroy = -4 deprecated;

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

Почему я в этом случае могу определить метод-расширение Add, не не могу — operator +? Почему нельзя сделать так, чтобы при ненайденном операторе + автоматически искался бы метод-расширение Add?
Скорее всего, это вопрос затрат и удобочитаемости. Тем более, что это можно уже сделать с помощью интерфейсов.

А вот generic constraint для числовых типов был бы очень кстати. Так можно было бы писать обобщенные арифметические функции без извращений типа dynamic:

public T Add<T>(T a, T b)
where T: numeric // например, так
{
    return a + b;
}

Для этого нужен не T: numeric, а T: has (T + T). И в F#, кстати, такое есть.

С точки зрения рантайма обобщенной операции сложения не существует, поэтому F# использует какой-то хитрый ход. Не уверен, но скорее всего типы разрешаются в момент компиляции, т.е. создается не честная generic-функция, а по конкретной реализации на каждый использованный при вызове этой функции тип T. Либо под капотом dynamic, хотя очень сомневаюсь.

После работы с Typescript вообще начинает не хватать структурной типизации. Но сомневаюсь, что ее приделают в C# в каком-либо обозримом будущем.

Обобщенного numeric в рантайме тоже нет. А уж если мечтать, то ограничения по методам намного удобнее.

Скорее всего, это вопрос затрат и удобочитаемости. Тем более, что это можно уже сделать с помощью интерфейсов.

И чем же a.Add(b.Multiply©) лучше a + b * c?
Написал идею:
https://visualstudio.uservoice.com/forums/121579-visual-studio-ide/suggestions/17335231-extend-operator-overloading
Например тем, что можно перейти к реализации метода с помощью ctrl+click и сразу узнать, что именно он делает. А к телу перегруженного оператора, насколько я знаю, так просто не перейдешь.

Вообще дизайн стандартной библиотеки C# склоняется к методам и интерфейсам. Если хотите примеров того, насколько неудобочитаемым становится код при активном использовании (кастомных) операторов — вот, например, когда-то мы писали парсер на F#
UFO just landed and posted this here
Как это? Подвёл курсор к плюсику, нажал на F12 и перешёл к оператору.
В идеале не нужно ходить к телу перегруженного оператора, он должен быть семантически очевиден. Если это не так, то перегружать оператор для этого типа не стоит.
CodeInjector выглядит круто — Никогда не понимал этот гемморой с INotifyPropertyChanged, а тут вполне карасивое решение.

Ещё мне очень нехватает строковых Enum-ов. Простая вещь, а сделает код более красивым и удобным в написании.
Ещё мне очень нехватает строковых Enum-ов

А можно пример?
Пожалуйста

Вместо:
public static class StringEnum
{
public const string ValueA = «A»;
public const string ValueB = «B»;
}

public string Field {get; set;} = StringEnum.ValueA;

Сделать так:
public enum StringEnum
{
ValueA = «A»,
ValueB = «B»
};

public StringEnum Field {get; set;} = StringEnum.ValueA;

Преимущества:
— одинаковый подход к перечисляемым типам
— точно знаешь что присваивать в поле (т.к. в текущей реализации — это просто строка и можно присвоить что угодно, а в предлагаемой — только значения из списка)

Может быть вообще можно расширить до объектного enum-а (т.е. создавать перечисления любого типа) — не думаю что это будет часто-используемая фича, но вполне полезная.
Строка — не перечисляемый тип.
В каких случаях второй пункт может быть использован? Почему нужно присваивать именно строку, а не значение enum, которое позже преобразуется в строку?
Преимущества:
— одинаковый подход к перечисляемым типам
— точно знаешь что присваивать в поле (т.к. в текущей реализации — это просто строка и можно присвоить что угодно, а в предлагаемой — только значения из списка)


А почему бы просто не пользоваться Енумом в том виде, который сейчас есть? Ну вот допустим
enum VehicleType {
  Light,
  Medium,
  Heavy
}

class Vehicle {
  private readonly VehicleType type;
 
  public Vehicle (VehicleType type) {
    this.type = type;
  }

  public string GetImageUrl () {
    return "/images/" + type + ".png";
  }
}

new Vehicle(VehicleType.Heavy).GetImageUrl(); // "/images/Heavy.png"


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

enum ExtendedVehicleType : VehicleType {
  Spg, Spatg
}

new Vehicle(VehicleType.Spg);


Приходится для такого делать костыль вместо енума, а это имеет ряд своих недостатков:
class VehicleType {
  public static readonly Light  = new VehicleType("Light");
  public static readonly Medium = new VehicleType("Medium");
  public static readonly Heavy  = new VehicleType("Heavy");

  // ..
}


Может есть какой-либо вменяемый способ это использовать в c#?
А почему бы просто не пользоваться Енумом в том виде, который сейчас есть?

Как бы сделать (де)сериализацию того, что нельзя выразить идентификатором C# (пробелы, пунктуация и прочее)? Конечно, сейчас можно навесить кучу аттрибутов под каждый используемый сериализатор, но это смотрится плохо.
Я видел два варианта, кроме идентификаторов

Привычный — объявить enum в классе и рядом положить статический Dictionary<enum, string > с нужными строками(некрасиво)
Красивый — использовать атрибуты.
Атрибуты терпимы, когда сериализатор один. Зоопарк из атрибутов сложно назвать красотой.
В данном случае тип атрибута — один, например можно для этого использовать DescriptionAttribute(он стандартный).
Другое дело, что
    public static string GetDescription(this Enum value)
    {
      DescriptionAttribute[] descriptionAttributeArray = (DescriptionAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof (DescriptionAttribute), false);
...

работает медленно, и enum вида
 private enum Verdict : byte {
            [Description(" ")]
            Space,
            [Description(",")]
            Comma
        }

не всех стилистически радует.
Вы уверены, что все сериализаторы ищут DescriptionAttribute на каждом enum и с радостью будут преобразовывать enum <--> string? Или мне самому их обучать?
Всё, наконец-то я понял, что вы имеете ввиду, у вас есть некие стандартные сериализаторы в некие строковые файлы, и сериализация в Enum-овское нестроковое value или "." вас не устроит, нужно чтобы value было строковым.
Тогда да, перечислимые строки были бы решением, если просто строковые константы не устраивают.
А так у вас будет зоопарк энумов, если сериализаторы называют поля по разному.
Сериализаторам без разницы, как называются поля. Допустим, что это JSON, они знают, что тип — enum, что у него строковый базовый тип => его нужно сериализовать в соответствующую строку. Выглядело это бы так:

enum MyStrings : string {
    One = "One with whitespaces",
    Two = "Two with whitespaces",
    Three = "Three with whitespaces"
}

MyStrings enVal = MyStrings::One;

var json = JsonConverter.Serialize<string>(enVal);
Ну во-первых, всё таки
var json = JsonConverter.Serialize<MyStrings>(enVal);


А во вторых — с аттрибутами он может выглядеть точно так же.

upd:
На самом деле вот так:
var json = JsonConverter.Serialize(enVal);

Тип шаблона может резолвиться автоматом.
Да, верно. С атрибутами он может выглядеть подобно. Только будет с атрибутами)

И нет таких стандартных атрибутов, которые я бы мог использовать везде. Поэтому будут вешать свой кастомный для каждого сериализатора. И в этом соль.
UFO just landed and posted this here
Асинхронный Main

В C# очень неудачная реализация async/await, идея хорошая но перемудрили конкретно. Поэтому в Main всё это не работает, хотя и должно по задумке. Весь интернет пестрит непонятками.

Что касается генерации кода это прекрасно, однако как я могу судить — снова перемудрили и теперь нужно будет при отладке большого проекта ещё и искать «кто же включает очередную функцию в этот класс». Самая верная реализация генерации это у Хаскеля.
А мне, наоборот, реализация асинхронности в C# очень нравится, а конкретно генерация state machine из асинхронных функций — руками их писать очень и очень тяжело. Альтернативой ей является использование более сложных в отладке fibers (например, coroutines в C++).

А неудачной я считаю реализацию планировщика. Главная прелесть асинхронности — выполнение всех задач в одном потоке, что существенно снижает расходы на межпотоковую синхронизацию и переключение между ними.
Но в C# используется либо ThreadPool (по умолчанию), либо циклы сообщений WinForms/WPF. Всё это не даёт большого выигрыша в производительности от использования асинхронности, а при низкой гранулярности задач производительность ещё и падает.

Как итог — в своих проектах я использую собственный планировщик и собственные обёртки над сокетами. Заодно получаю полноценную кроссплатформенность — при запуске под Windows используется IOCP, под Linux — epoll.
UFO just landed and posted this here
Если вы напишете await asyncFunction() то не будет выделено нового потока, если внутри asyncFunction он нигде явно не выделяется

Это зависит от текущего SynchronizationContext. Обратите внимание, что по ссылке приведён конкретный пример работы асинхронных вызовов для Windows Forms.
Это зависит от текущего SynchronizationContext

Это зависит в первую очередь от того, что вернула asyncFunction, и если она вернула уже выполненный таск, то SynchronizationContext вообще не будет использован.

А если вернула незавершённую задачу (что является гораздо более частным случаем), то продолжение её выполнения таки зависит от SynchronizationContext.

Следующий код будет выдавать разный результат в зависимости от контекста:
Console.WriteLine(Thread.CurrendThread.ManagedThreadId);
await Task.Yield();
Console.WriteLine(Thread.CurrendThread.ManagedThreadId);


Если оформить его как асинхронную функцию и вызвать напрямую из Main (Task.Run.Wait()), то вывод на экран будет разный.

Замечание 1: ManagedThreadId разный для разных потоков.
Замечание 2: вместо Task.Yield может быть любая задача, не являющаяся автоматически завершённой.

Не совсем так. Task.Yield — это все-таки не задача, а требование уступить поток. Там отдельный Awaiter написан для него.


Если смотреть задачи — то они как раз работают довольно адекватно. При установленном контексте синхронизации метод в него возвращается. А без контекста синхронизации — метод продолжает исполняться в том же потоке, который задачу выполнил (исключение — если синхронное продолжение было явно запрещено в свойствах задачи).

«Ожидать» с помощью await можно любой объект (не обязательно Task), имеющий метод GetAwaiter, возвращающий объект, имеющий три метода (OnCompleted, IsCompleted, GetResult). Один из методов — OnCompleted(Action continuation), вызывается по завершении задачи и приводит к продолжению выполнения кода, вызвавшего await.

Вот здесь можно посмотреть реализацию этого метода: если контекст задан, то вызывается функция помещения задачи в очередь из контекста (Post), а если контекта нет, то продолжение задачи планируется к выполнению в пуле потоков.

Аналогично можно посмотреть на Task.GetAwaiter, но там чёрт ногу сломит. В конечном итоге вызывается TaskContinuation.Run, который вызывает всё тот же Post или лезет в ThreadPool.

Для YieldAwaiter приведенное поведение — ожидаемое!


Что же до TaskAwaiter, то тут все тоже просто, если разобраться. Если не было захвачено ни контекста синхронизации, ни планировщика задач — то используется класс AwaitTaskContinuation. Этот класс по возможности вызывает продолжение синхронно.

Видимо, он это делает слишком редко, например, когда задача в момент выполнения await уже завершена. Просто вставьте в Main следующий код и посмотрите на результат:

            Task.Run(async () =>
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                await Task.Delay(10);
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            }).Wait();


Но это не беда. Простой планировщик решает эту проблему.

А как, по-вашему, этот код вообще может работать? После истечения 10ти миллисекунд выполнение обязано вернуться в пул потоков. Почему для вас так важно, чтобы это был тот же самый поток? Все потоки пула одинаковы.


Лучше посмотрите на вот этот код:


public static void Main()
{
    A().Wait();
}

static async Task A() {
    await B();
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

static async Task B() {
    await C();
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

static async Task C() {
    await Task.Yield();
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

В данном случае, все три оператора Console.WriteLine выполнятся в одном и том же потоке, что показывает, что продолжения и правда вызываются синхронно.

В данном случае, все три оператора Console.WriteLine выполнятся в одном и том же потоке, что показывает, что продолжения и правда вызываются синхронно.

Ага, только у меня периодически получается так:
4
5
5


Почему для вас так важно, чтобы это был тот же самый поток?

Потому что я хочу, чтобы все задачи работали последовательно в одном потоке:
1. Нет затрат на синхронизацию между потоками — никаких блокировок и ожиданий, мьютексы и эвенты не нужны.
2. Нет затрат на переключение контекста между потоками в планировщике. Делегировать выполнение задачи в другой поток — это очень долго: за то время, пока поток-воркер проснётся, пока сообщит другому потоку о готовности результата, можно выполнить с сотню задач в основном потоке.

Когда я написал свой планировщик, пиковое количество выполняемых сетевых операций в секунду выросло больше, чем на порядок.

"периодически" — это как?

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

Запустил код на трех разных компьютерах. Каждый раз все три числа совпадали… Вы что-то делаете не так.

Многопоточность — она такая неожиданная. Вот вроде всё работает, а потом опа — race condition.
А код уже написан и оттестирован — тесты проходят, а в продакшене почему-то всё падает.

Если вы вызовете 3 раза подряд ThreadPool.QueueЧтоТоТам, то эти 3 задачи могут быть запущены как на одном потоке, так и на нескольких, никакой гарантии здесь нет.

Не надо объяснять мне что такое race condition, я это знаю.


В коде, который я привел, окончания всех трех задач всегда выполняются в одном и том же потоке. Если они оказались в разных потоках — вы что-то сделали не так.

В коде, который я привел, окончания всех трех задач всегда выполняются в одном и том же потоке

Если они всегда выполняются конкретно на вашем компьютере в одном и том же потоке, это ещё ничего не значит:
http://funkyimg.com/i/2kCET.png

Запустите 10, 20, 30 раз, попробуйте debug/release поменять, запускать без отладчика. Это именно race condition.

Что такое Utilities.Asynchronius? Если убрать этот using и все лишние референсы — ошибка останется?

Это мой планировщик от библиотечки. Да, на голом проекте эффект сохраняется. На разных системах, даже под Mono.

Вот как раз Mono — совсем не показатель, там запросто может быть более слабая реализация.

Ну вот ещё что, зависеть от конкретной реализации конкретного фреймворка. Спасибо, не надо, мне это код ещё и поддерживать надо. Если в документации явно не указано конкретное поведение, то не стоит верить наблюдениям — они могут быть обманчивы.

Вот мой скриншот:


картинка

Imgur


Отсюда прекрасно видно, что продолжения были вызваны синхронно. Если у вас меняется номер потока — значит, что-то мешает синхронному вызову. Какой-нибудь extension-метод, который перекрыл системный, или класс. Или просто установленный контекст синхронизации. Или хитрый аспект, который что-то нарушил.


Ну, или же просто какой-то необычный рантайм.

Ага, 1, 3, 5, 7 — простые, значит, и 9 — тоже простое.

Выполнение программы детерминировано.

Работа планировщика в многопоточной среде не является детерминированной.

Объясните же, а?
http://funkyimg.com/i/2kCUY.png

Оно не должно попадать в планировщик.


PS не буду вам больше отвечать, потому что вы спорите для того чтобы спорить, а не чтобы разобраться в чем дело.

Конечно, не должен: по завершении задачи управление сразу передаётся в код, который вызывает continuation. Но почему в некоторых случаях continuation вызывается не сразу, а попадает в ThreadPool, мне не понятно, и я хочу в этом разобраться. Ваши же ответы выглядят как «ничего не знаю, у меня всё работает». Я же жду объяснений.

Скорее всего, причина в разных фреймворках, но зависеть от поведения в конкретном фреймворке, к тому же недокументированного, я считаю неправильным.

На самом деле, ответ значения не имеет. Для меня важно то, что в стандартном планировщике (не Forms/WPF) после любых IO операций, которые не могут быть завершены прямо сейчас, продолжение задачи осуществляется не в вызвавшем потоке, а когда таких задач несколько — в разных потоках.
Чтобы окончательно разрешить спор, выкладываю проект.

Мои результаты:

Debugger: not attached
.NET version: 4.0.30319.42000
Platform: 64 bit

Same ThreadId: 98,94%


Получается, что в 1 случае из 100 программа ведёт себя не так, как должна себя вести. Это типичный пример неверного предположения о работе программы в многопоточной среде, которое может привести к очень неприятным и трудноуловимым ошибкам.
Здравствуйте, а можно где-то посмотреть на код вашей реализации планировщика?

По-моему, вы только что продемонстрировали, что модель TPL в .net сделана удобно: для вашей конкретной задачи стандартный планировщик не подошел (это не значит, что он не подойдет другим, у меня он прекрасно работает) — и вы легко заменили его на свой.

Не совсем понятна идея добавления таких вещей, как возможность писать код в виде строк и докомпилировать его или вклиниваться в вызов метода и делать что-то с ним такое, что уже сделать нельзя используя полиморфизм, переопределение в языке каким изначально задумывался C#(строготипизированным и со статической типизацией).
Сейчас там же можно вызвать наследуемый PropertyChanged метод без дубликтов логики или применить логирование через action filter в крайнем случае dynamic proxy, благо для AOP в дотнете инструментария уже очень много.
С трудом вериться с учетом сопровождение этого кода, тестируемости, переносимость, использование АПИ привязанного к конкретному комплиятору, оно реально удобней — кроме что разве красивого и на первый взгял гибкого функционала.

А что не так с "конкретным компилятором" если сам компилятор — переносимый и открытый?

Дело не в компиляторе самом, а стандартах. В дотнете/с# уже написано масса хороших библиотек и кода, что используют для аналогичных целей reflection.emit, DLR, Dymamic Proxies, Expressions. мс продолжают добавлять новые инструменты и менять подходы раз в несколько лет, для весьма странных вещей без всякой на то нужды. Количество задач где целесообразно это променять скоро станет меньше количества инструментов, которыми их можно решить — и все это надо будет саппортить в будущем для обратной совместимости? Как считаете это способствующий развитию и гибкости языка момент?
Если на проекте уже надо совсем прямо надо докмпилировать какой-то код из строки, это даже сейчас аналогичным образом можно сделать применив T4 шаблоны или написав автоматизацию вызова csc.exe, зачем это добавлять нативно в компилятор?

Чтобы перестать писать T4, например.

>> Добавление генерирующего код расширения для компилятора
Ааа, вот же лютый костыль, почему бы не сделать множественное наследование.

>> Superseding members — замещение модификатора для того, чтобы дать больше возможностей инструментам генерации кода
опять же приведенный механизм использования нормально решается множественным наследованием

И вместо адекватных трейтов для того же INotifyPropertyChanged, будет просто костыль, который непонятно как работает, непонятно где цепляется, с кодом не содержащем подсветку синтаксиса, с неработающими фичами навигации в среде разработки, без работающего Intellisense. Без возможности покрытия тестами. Как говорится: «Счастливой отладки».
Почему бы не сделать статические generics, на манер шаблонов C++? Нередко наблюдал, как люди из-за отсутствия оных используют dynamic (со всеми вытекающими проблемами в рантайме).
Можно пример? Статические классы и методы могут быть generics.
Видимо, здесь речь идёт не о статических методах, а о подстановке типов и кодогенерации на этапе компиляции, что, по идее, должно дать выигрыш в производительности по сравнению с дженериками, но за счёт сильного раздутия кода. Ну и об отсутствии интерфейсов типа IAddable для примитивных типов.
… но за счёт сильного раздутия кода

Extern templates же.
Суть в разнице между дженериками и шаблонами. Первые полагаются на иерархию наследования/реализации интерфейсов, а вторые — буквально генерируют код на этапе компиляции и тут же подвергают его статическому анализу.

Вкупе с variadic templates и развитым выводом типов это дает возможность писать крайне гибкий код. И это уже есть в C++/CLI.

Вот только пользоваться этим в C++ зачастую неудобно. Расширения компилятора выглядят лучше.

Сложно представить современный C++ без шаблонов.

Расширения компилятора выглядят лучше.

Очень спорно. Семантически это больше на миксины похоже, чем на шаблоны.

Так ведь это зависит от целей использования. Расширение компилятора позволяет сделать и миксин, и шаблон.

Судя по примерам в статье это всегда будет миксин. Шаблон в C++ всегда создает новую сущность (функция, класс или синоним типа), а эта штука, как я понял из статьи, инжектит новые поля/методы в уже существующие типы.

Расскажите, пожалуйста, чем же это удобнее пресловутых шаблонов?
  1. Логика работы расширений описывается на том же самом языке, для которого эти расширения пишутся.


    Шаблоны в c++ — это отдельный функциональный (!) язык (мета)программирования, при том что основной язык — императивный...


  2. В расширении можно произвольно управлять наличием и именами методов — в то же время шаблоны C++ ограничены заданным при их написании набором методов, а управлять их наличием нужно через местами контринтуитивный механизм SFINAE.
В C++ с помощью SFINAE затыкают полное отсутствия механизма, вроде where-констрейнтов из C# + Reflection в некоторой степени. Можно же найти золотую середину, имея поддержку обеих технологий на платформе. Допустим, ввести специальный синтаксический assert-блок или дать возможность как-то формализовать требования к типу в отдельный метатип, в котором можно наделать дополнительных запросов к типу, посредством compile-time аналога рефлекшна.

И да, описывать требования удобнее на декларативном языке, как это реализовано для дженериков в C#.

Шаблоны в c++ — это отдельный функциональный (!) язык (мета)программирования, при том что основной язык — императивный...

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

А в чем отличие от шаблонов? Наличие методов определяется и без каких-то особых трюков с SFINAE. Причем, мне кажется, расширения компилятора здесь работают так же. А переименовывать вы можете так же свободно.
Надавать по рукам за такие «новые» возможности. Код превратиться в кучу гуано!
Можно добавить контекстное ключевое слово field в методах доступа свойства.
А также пустой nameof(), который возвращает имя текущего свойства/метода:
public string Property
{
    get { return field; }
    set
    {
        if (field == value) return;
        field == value;
        OnPropertyChanged(nameof());
    }
}
Я сейчас еретическую вещь напишу: больше всего в C# мне не хватает препроцессора. Проблема с INotifyPropertyChanged решалась бы одной строчкой.
А также пустой nameof(), который возвращает имя текущего свойства/метода:
А это уже есть, но немного по-другому записывается:
void OnPropertyChanged([CallerMemberName] string propertyName = "") { .. }
...
public string Property1
{
    set
    {
        OnPropertyChanged();
    }
}
public string Property2
{
    set
    {
        OnPropertyChanged();
    }
}

https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.callermembernameattribute(v=vs.110).aspx
Sign up to leave a comment.

Articles