Наследование, композиция, агрегация

Нередко случается, что решив разобраться с какой-то новой темой, понятием, инструментом программирования, я читаю одну за другой статьи на различных сайтах в интернете. И, если тема сложная, то эти статьи могут не на шаг не приблизить меня к понимаю. И вдруг встречается статья, которая моментально дает озарение и все паззлы складываются воедино. Трудно определить, что отличает такую статью от других. Правильно подобранные слова, оптимальная логика изложения или же просто более релевантный пример. Я не претендую на то, что моя статься окажется новым словом в C# или же лучшей обучающей статьей. Но, возможно для кого-то она станет именно той, которая позволит разобраться, запомнить и начать правильно применять те понятия, о которых пойдет речь.

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

```class Vehicle
{
    bool hasWheels;
}

class Car : Vehicle
{
    string model = "Porshe";
    int numberOfWheels = 4
}```

Ассоциация – это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель. Вполне естественно, что он не будет являться наследником двигателя (хотя такая архитектура тоже возможна в некоторых ситуациях).

Выделяют два частных случая ассоциации: композицию и агрегацию.

Композиция – это когда двигатель не существует отдельно от автомобиля. Он создается при создании автомобиля и полностью управляется автомобилем. В типичном примере, экземпляр двигателя будет создаваться в конструкторе автомобиля.

```
class Engine
{
    int power;
    public Engine(int p)
    {
        power = p;
    }
}

class Car
{
    string model = "Porshe";
    Engine engine;
    public Car()
    {
        this.engine = new Engine(360);
    }
}
```

Агрегация – это когда экземпляр двигателя создается где-то в другом месте кода, и передается в конструктор автомобиля в качестве параметра.

```
class Engine
{
    int power;
    public Engine(int p)
    {
       power = p;
    }
}        

class Car
{
    string model = "Porshe";
    Engine engine;
    public Car(Engine someEngine)
    {
         this.engine = someEngine;
    }
}

Engine goodEngine = new Engine(360);
Car porshe = new Car(goodEngine);

```

Хотя ведутся дискуссии о преимуществах того или иного способа организации взаимодействия между классами, какого-либо абстрактного правила не существует. Разработчик выбирает тот или иной путь основываясь на элементарной логике (“является” или “имеет”), но также принимает во внимание возможности и ограничения, которые дают и накладывают эти способы. Для того, чтобы увидеть эти возможности и ограничения, я попытался написать пример. Достаточно простой, чтобы код оставался компактным, но и достаточно развитый, чтобы в рамках одной программы можно было применить все три способа. И, главное, я попытался сделать этот пример как можно менее абстрактным – все объекты и экземпляры понятны и осязаемы.

Напишем простенькую игру – танковый бой. Играют два танка. Они поочередно стреляют и проигрывает тот, здоровье которого упало до нуля. В игре будут различные типы снарядов и брони. Для того, чтобы нанести урон необходимо во-первых, попасть по танку противника, во-вторых, пробить его броню. Если броня не пробита, урон не наносится. Логика игры построена на принципе «камень-ножницы-бумага»: то есть броня одного типа хорошо противостоит снарядам определенного типа, но плохо держит другие снаряды. Кроме того, снаряды, которые хорошо пробивают броню, наносят малый «заброневой» урон, и, напротив, наиболее «летальные» снаряды имеют меньше шансов пробить броню.

Создадим простенький класс для пушки. Он будет иметь два приватных поля: калибр и длину ствола. От калибра зависит урон, и, частично, способность к пробитию брони. От длины ствола – точность стрельбы.

```
public class Gun
{
    private int caliber;
    private int barrelLength;
} 
```

Сделаем также конструктор для пушки:

```
public Gun(int cal, int length)
{
    this.caliber = cal;
    this.barrelLength = length;
}
```

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

```
public int GetCaliber()
{
    return this.caliber;
}
```

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

```
public bool IsOnTarget(int dice)
{
    return (barrelLength + dice) > 100; 
}
```

Целиком класс пушки выглядит следующим образом:

```
public class Gun
{
   private int caliber;
   private int barrelLength;

   public Gun(int cal, int length)
   {
       this.caliber = cal;
       this.barrelLength = length;
   }

   public int GetCaliber()
   {
       return this.caliber;
   }

   public bool IsOnTarget(int dice)
   {
       return (barrelLength + dice) > 100; 
   }
}
```

Теперь сделаем снаряды – это наиболее очевидный случай для применения наследования, но и агрегацию в нем тоже применим. Любой снаряд имеет свои особенности. Просто неких гипотетических снарядов не бывает. Поэтому класс делаем абстрактным. Делаем ему строковое поле «тип».

Снаряды делают для пушек. Для определенных пушек. Снаряд одного калибра не выстрелит из пушки другого калибра. Поэтому добавляем снаряду поле-ссылку на экземпляр пушки. Делаем конструктор.

```
public abstract class Ammo
{
   Gun gun;
   public string type;
   public Ammo(Gun someGun, string type)
   {
       gun = someGun;
       this.type = type;
   }
}
```

Здесь мы применили агрегацию. Где-то будет создана пушка. Потом к этой пушке будут создаваться снаряды, которые имеют указатель на пушку.

Конкретные типы снарядов будут наследниками абстрактного снаряда. Наследники могут просто наследовать методы родителя, но могут и быть переопределены, то есть работать не так, как родительский метод. Но мы точно знаем, что любой снаряд должен иметь ряд методов. Любой снаряд должен наносить урон. Метод GetDamage просто возвращает калибр, умноженный на три. В общем случае, урон снаряда зависит от калибра. Но этот метод будет переопределяться в дочерних классах (помним, что снаряды, которые хорошо пробивают броню, как правило наносят меньший «заброневой» урон. Чтобы иметь возможность переопределить метод в дочернем классе, используем слово virtual.

```
public virtual int GetDamage()
{
      //TO OVERRIDE: add logic of variable damage depending on Ammo type
      return gun.GetCaliber()*3;
}
```

Любой снаряд должен пробивать (или по крайней мере пытаться пробить) броню. В общем случае способность пробивать броню также зависит от калибра (ну, и еще от многого – начальной скорости, например, но мы не будем усложнять). Поэтому, метод возвращает калибр. То есть, грубо говоря, снаряд может пробить броню, равную по толщине своему калибру. Этот метод не будет переопределяться в дочерних классах.

```
public int GetPenetration()
{
      return gun.GetCaliber();
}
```

Кроме того, для удобной отладки и организации консольного вывода, имеет смысл добавить метод ToString, который просто позволит нам увидеть, что это за снаряд и какого калибра:

```
public override string ToString()
{
      return $"Снаряд " + type + " к пушке калибра " + gun.GetCaliber();
}
```

Теперь сделаем разные типы снарядов, которые будут наследовать абстрактный снаряд: фугасный, кумулятивный, подкалиберный. Фугасный наносит самый большой урон, кумулятивный – меньше, подкалиберный – еще меньше. Дочерние классы не имеют полей и вызывают конструктор базового снаряда, передавая ему пушку, и строковый тип. В дочернем классе переопределяется метод GetDamage() – вносятся коэффициенты, которые увеличат или уменьшат урон по сравнению с дефолтным.

Фугасный (дефолтный урон):

```
public class HECartridge : Ammo
{        
    public HECartridge(Gun someGun) : base(someGun, "фугасный") { }

    public override int GetDamage()
   {
       return (int)(base.GetDamage());            
   }
}
```

Кумулятивный (дефолтный урон х 0.6):

```
public class HEATCartridge : Ammo
{        
   public HEATCartridge(Gun someGun) : base(someGun, "кумулятивный") { }
        
   public override int GetDamage()
   {
            return (int)(base.GetDamage() * 0.6);
   }
}
```

Подкалиберный (дефолтный урон х 0.3):

```
public class APCartridge : Ammo
{      
   public APCartridge(Gun someGun) : base(someGun, "подкалиберный") {  }

   public override int GetDamage()
    {
        return (int)(base.GetDamage() * 0.3);
    }
}
```

Обратите внимание, что в переопределенном методе GetDamage вызывается и метод базового класса. То есть, переопределив метод, мы также сохраняем возможность обратиться к дефолтному методу, использовав ключевое слово base).

Итак, для снарядов мы применили и агрегацию (пушка в базовом классе), и наследование.
Создадим теперь броню для танка. Здесь применим только наследование. Любая броня имеет толщину. Поэтому абстрактный класс брони будет иметь поле thickness, и строковое поле type, которое будет определятся при создании дочерних классов.

```
public abstract class Armour
{
    public int thickness;
    public string type;

    public Armour(int thickness, string type)
    {
        this.thickness = thickness;
        this.type = type;
    }
}
```

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

```
public virtual bool IsPenetrated(Ammo projectile)
{
    return projectile.GetDamage() > thickness;
}
```

А пробита они или нет – зависит от того, какой прилетел снаряд: в дефолтном случае какого калибра. Поэтому метод принимает экземпляр снаряда и возвращает булевый результат: пробита или нет. Создадим несколько типов брони – наследников абстрактной брони. Приведу код лишь одного типа – логика примерно такая же, как и в снарядах. Гомогенная броня хорошо держит фугасный снаряд, но плохо – подкалиберный. Поэтому, если прилетел подкалиберный снаряд, который имеет высокую бронепробиваемость, то в вычислениях наша броня как-бы становится тоньше. И так далее: каждый вид брони имеет свой набор коэфициентов устойчивости к тому или иному снаряду.

```
public class HArmour : Armour
{
    public HArmour(int thickness) : base(thickness, "гомогенная") { }
    public override bool IsPenetrated(Ammo projectile)
    {
        if (projectile is HECartridge)
        {
        	//Если фугасный, то толщина брони считается больше
              return projectile.GetPenetration() > this.thickness * 1.2;
        }
        else if (projectile is HEATCartridge)
        {
                //Если кумулятивный, то толщина брони нормальная
                return projectile.GetPenetration() > this.thickness * 1;
        }
        else
        {
                //Если подкалиберный, то считаем уменьшаем толщину
                return projectile.GetPenetration() > this.thickness * 0.7;
        }
    }
}
```

Здесь мы используем одно из чудес, которые дает полиморфизм. Метод принимает любой снаряд. В сигнатуре указан базовый класс, а не дочерние. Но внутри метода, мы можем увидеть, что за снаряд прилетел – какого типа. И в зависимости от этого, реализуем ту или иную логику. Если бы мы не применили наследование для снарядов, а сделали просто три уникальных класса типов снарядов, то проверку пробития брони пришлось бы организовывать иначе. Нам пришлось бы писать столько перегруженных методов, сколько типов снарядов у нас в игре, и вызывать один из них в зависимости от того, какой снаряд прилетел. Это тоже было бы довольно изящно, но не относится к теме данной статьи.

Теперь у нас все готово для создания танка. В танке не будет наследования, но будет композиция и агрегация. Разумеется, у танка будет название. У танка будет пушка (агрегация). Для нашей игры сделаем допущение, что танк может «переодевать» броню перед каждым ходом – выбрать тот или иной тип брони. Для этого, у танка будет список типов брони. У танка будет боеукладка – список снарядов, который будет наполнен снарядами, созданными в конструкторе танка (композиция!). У танка будет здоровье (уменьшается при попадании в него), и, у танка будет текущая выбранная броня и текущий выбранный снаряд.

```
public class Panzer
{
    private string model;
    private Gun gun;
    private List<Armour> armours;
    private List<Ammo> ammos;        
    private int health;
                
    public Ammo LoadedAmmo { get; set; }
    public Armour SelectedArmour { get; set; }
}
```

Для того, чтобы конструктор танка остался более-менее компактным, сделаем два вспомогательных приватных метода, которые добавляют три типа брони соответствующей толщины, и наполняют боеукладку 10 снарядами каждого из трех типов:

```
private void AddArmours(int armourWidth)
{
     armours.Add(new SArmour(armourWidth)); 
     armours.Add(new HArmour(armourWidth));
     armours.Add(new CArmour(armourWidth));
}

private void LoadAmmos()
{
     for(int i = 0; i < 10; i++)
     {
          ammos.Add(new APCartridge(this.gun));
          ammos.Add(new HEATCartridge(this.gun));
          ammos.Add(new HECartridge(this.gun));
     }
}
```

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

```
public Panzer(string name, Gun someGun, int armourWidth, int h)
{
    model = name;
    gun = someGun;
    health = h;
    armours = new List<Armour>();
    ammos = new List<Ammo>();
    AddArmours(armourWidth);
    LoadAmmos();
    LoadedAmmo = null;
    SelectedArmour = armours[0]; //по умолчанию - гомогенная броня
}```

Обратите внимание, что здесь мы снова используем возможности полиморфизма. Наша боекладка вмещает снаряды любого типа, так как список имеет тип данных Ammo – родительский снаряд. Если бы мы не наследовались, а создавали уникальные типы снарядов, пришлось бы делать отдельный список под каждый тип снаряда.

Пользовательский интерфейс танка состоит из трех методов: выбрать броню, зарядить пушку, выстрелить.

Выбрать броню:

```
public void SelectArmour(string type)
{
     for (int i = 0; i < armours.Count; i++)
     {
         if (armours[i].type == type)
         {
             SelectedArmour = armours[i];
             break;
         }
     }
}
```

Зарядить пушку:

```
public void LoadGun(string type)
{
     for(int i = 0; i < ammos.Count; i++)
     {
         if(ammos[i].type == type)
         {
             LoadedAmmo = ammos[i];
             Console.WriteLine("заряжено!");
             return;
         }
     }
     Console.WriteLine($"сорян, командир, " + type + " закончились!");
}
```

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

Выстрелить:

```
public Ammo Shoot()
{
    if (LoadedAmmo != null)
    {                
       Ammo firedAmmo = (Ammo)LoadedAmmo.Clone();
       ammos.Remove(LoadedAmmo);
       LoadedAmmo = null;
       Random rnd = new Random();
       int dice = rnd.Next(0, 100);
       bool hit = this.gun.IsOnTarget(dice);
        if (this.gun.IsOnTarget(dice))
        {
             Console.WriteLine("Попадание!");
             return firedAmmo;
        }
        else
        {
             Console.WriteLine("Промах!");
             return null;
        }              
     }
     else Console.WriteLine("не заряжено");
     return null;
 }
```

Здесь – поподробнее. Во-первых, есть проверка заряжена ли пушка. Во-вторых, снаряд, который вылетел из ствола, уже не существует для данного танка, его уже нет ни в пушке, ни в боеукладке. Но физически он еще существует – летит по направлению к цели. И если попадет, будет участвовать в вычислении пробития брони и урона цели. Поэтому, мы сохраняем этот снаряд в новой переменной: Ammo firedAmmo. Поскольку на следующей же строке данный снаряд перестанет существовать для данного танка, придется использовать интерфейс IClonable для базового класса снаряда:

```
public abstract class Ammo : ICloneable
```

Этот интерфейс требует реализации метода Clone(). Вот она:

```
public object Clone()
{
     return this.MemberwiseClone();
}
```

Теперь все супер реалистично: при выстреле генерируется dice, пушка рассчитывает попадание своим методом IsOnTarget, и, если попадание есть, то метод Shoot вернет экземпляр снаряда, а если промах – то вернет null.

Последний метод танка – его поведение при попадании вражеского снаряда:

```
public void HandleHit(Ammo projectile)
{
     if (SelectedArmour.IsPenetrated(projectile))
     {
         this.health -= projectile.GetDamage();                
     }
     else Console.WriteLine("Броня не пробита.");
}
```

Снова полиморфизм во всей красе. К нам прилетает снаряд. Любой. Исходя из выбранной брони и типа снаряда, вычисляется пробита броня или нет. Если пробита, то вызывается метод конкретного типа снаряда GetDamage().

Все готово. Остается только написать консольный (или неконсольный) вывод, в котором будет обеспечен пользовательский интерфейс и в цикле реализованы поочередные ходы игроков.

Подведем итоги. Мы написали программу, в которой использовали наследование, композицию и агрегацию, надеюсь, поняли и запомнили различия. Активно задействовали возможности полиморфизма, во-первых, когда любые экземпляры дочерних классов можно сложить в список, имеющий тип данных родительского, а во-вторых, создавая методы, которые принимают в качестве параметра родительский экземпляр, но внутри которых вызываются методы дочернего. По ходу текста я упоминал возможные альтернативные реализации – замену наследования на агрегацию, и, универсального рецепта тут нет. В нашей реализации наследование дало нам легкость добавления новых деталей в игру. Например, чтобы добавить новый тип снаряда нам нужно лишь:

  • собственно, скопировать один из существующих типов, заменив название и строковое поле, передаваемое в конструктор;
  • добавить еще один if в дочерние классы брони;
  • добавить дополнительный пункт в меню выбора снаряда в пользовательском интерфейсе.

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

Ниже – приведена диаграмма наших классов.



В финальном коде игры все «магические числа», которые использовались в тексте, вынесены в отдельный статический класс Config. К публичным полям статического класса мы можем обратиться из любого фрагмента нашего кода и его экземпляр не нужно (и невозможно) создавать. Вот так он выглядит:

```
public static class Config
    {
        public static List<string> ammoTypes = new List<string> { "фугасный", "кумулятивный", "подкалиберный" };
        public static List<string> armourTypes = new List<string> { "гомогенная", "разнесенная", "комбинированная" };

        //трешхолд для пушки - величина, выше которой будем считать, что снаряд попал в цель
        public static int _gunTrashold = 100;

        //дефолтный коэффициент для заброневого действия базового снаряда
        public static int _defaultDamage = 3;

        //коэффициенты урона для снарядов разных типов
        public static double _HEDamage = 1.0;
        public static double _HEATDamage = 0.6;
        public static double _APDamage = 0.3;

        //коэффициенты стойкости брони
        //для гомогенной:
        //Если в гомогенную броню прилетает фугасный, то ее толщина считается большей -  коэффициент 1.2
        public static double _HArmour_VS_HE = 1.2;
        //Если в гомогенную броню прилетает кумулятивный, то ее толщина считается нормальной -  коэффициент 1.0
        public static double _HArmour_VS_HEAT = 1.0;
        //Если в гомогенную броню прилетает подкалиберный, то ее толщина считается меньшей -  коэффициент 0.7
        public static double _HArmour_VS_AP = 0.7;

        //для комбинированной брони
        //Если в комбинированную броню прилетает фугасный, то ее толщина считается нормальной -  коэффициент 1
        public static double _СArmour_VS_HE = 1.0;
        //Если в комбинированную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8
        public static double _СArmour_VS_HEAT = 0.8;
        //Если в комбинированную броню прилетает фугасный, то ее толщина считается больше - коэффициент 1.2
        public static double _СArmour_VS_AP = 1.2;

        //Для разнесенной брони
        //Если в разнесенную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8
        public static double _SArmour_VS_HE = 0.8;

        //Если в  разнесенную броню прилетает кумулятивный, то ее толщина считается больше - коэффициент 1.2
        public static double _SArmour_VS_HEAT = 1.2;

        //Если в  разнесенную броню прилетает подкалибереый, то ее толщина считается нормальной - коэффициент 1
        public static double _SArmour_VS_AP = 1.0;
    }
```

И благодаря этому классу мы можем производить дальнейшую настройку, меняя параметры лишь здесь, без дальнейшего углубления в классы и методы. Если, например, мы пришли к выводу, что подкалиберный снаряд получился слишком сильным, то мы меняем одну циферку в Config.
Весь код игры можно увидеть вот здесь.
Поделиться публикацией

Похожие публикации

Комментарии 97
    +2
    Агрегация – это когда экземпляр двигателя создается где-то в другом месте кода, и передается в конструктор автомобиля в качестве параметра.

    А откуда вы взяли это магическое определение?

      +1
      В объектно-ориентированных языках программирования существует три способа организации взаимодействия между классами. [...] Наследование описывается словом «является».

      А почему никак не учтены интерфейсы? Потому что между интерфейсом и реализующим его классом тоже отношение "is a" (то, что вы называете "является"), но называть это наследованием не стоило бы.

        –3
        наследованием не стоило бы

        Интерфейсы совершенно к ООП не относятся (ну то есть ООП возможно и без интерфейсов), хотя присутствуют в ООП языках.
          +3

          В этом смысле и классы к ООП не относятся (ООП возможно и без классов). Почему классы рассматриваем, а интерфейсы — нет?

            –4
            классы к ООП

            Пардон, как вы без типа будете что-то наследовать? Да я в курсе, что классы в тот же Cи внедрялись хитрыми макросами. Но был ли тогда ООП — большой вопрос, скорее более продвинутая группировка логически связанных методов.
            Или я ваш посыл не понял.
              +3
              Пардон, как вы без типа будете что-то наследовать?

              Ну так и наследование для ООП не обязательно.

                –5
                Ну так и наследование для ООП не обязательно

                Ну так и для автомобиля шины не обязательны. Он поедет и без них, но только долго ли выдержит и далеко ли уедет.
                Никто в здравом уме не будет реализовывать современный ООП язык без наследования (ну или не включить в ближайшей версии).
                  +3
                  Никто в здравом уме не будет реализовывать современный ООП язык без наследования

                  (Аргумент истинного шотландца?)


                  Вы про Go не слышали?


                  Или, вот, скажем, JS тоже вполне себе ОО-язык, а классов там не было (я, если честно, не помню, завезли ли).


                  Зачем конкретно, по-вашему, нужно наследование ОО-языку?

                    –2
                    Вы про Go не слышали?

                    Тут другой вопрос, является ли Go — ООПшным языком. Но на эту тему я холиварить не хочу. Т.к. у него свои границы применения, ну как у функциональных языках — вполне могут себе позволить обходится без многих ООПешностей.

                    JS тоже вполне себе ОО-язы

                    Ну так давно уже есть наследование, погуглите JavaScript extends. Ну вот первая ссылка developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes/extends
                      +2
                      Тут другой вопрос, является ли Go — ООПшным языком.

                      Я же говорю: аргумент истинного шотландца.


                      Ну так давно уже есть наследование, погуглите JavaScript extends

                      Наследование там было намного раньше. Классов не было.


                      Но: так зачем же ОО-языку наследование?

                        –4
                        Классов не было.

                        Вот вам пример класса, которого «не было» на чистом JS (в функциональном стиле):

                        function Animal(name) {
                          this.speed = 0;
                          this.name = name;
                        
                          this.run = function(speed) {
                            this.speed += speed;
                            alert( this.name + ' бежит, скорость ' + this.speed );
                          };
                        
                          this.stop = function() {
                            this.speed = 0;
                            alert( this.name + ' стоит' );
                          };
                        };
                        
                        var animal = new Animal('Зверь');
                          –1

                          Так это и не класс, это фабричная функция.

                            0
                            да нет, это определение и создание «прототипоориентированного объекта» prototype-based object. Которое в последствии получило синтаксический сахар в виде ключевого слова class.
                              0

                              … я и говорю: это не класс, это создание объекта.

                                0
                                что вы понимаете под словом «класс»? я лично — определенную конструкцию для описания какого-либо типа. Из которого можно потом создать объект.
                                я и говорю: это не класс

                                Ну т.е. класс из нового JS вы за класс и не считаете.
                                  –1
                                  Потому что это и не класс никакой. Это просто синтаксический сахар над прототипным наследованием
                                    0
                                    Это понятно, что JS это прототипы. А класс — это просто способ описания.
                                    Ну так что вы понимаете под словом «класс».

                                    .
                                    Это просто синтаксический сахар над прототипным наследованием

                                    И почитайте, что я писал две ветки назад
                                    Которое в последствии получило синтаксический сахар в виде ключевого слова class.

                                      0
                                      Класс — это описание пользовательского типа данных. Экземпляр класса — всегда остается экземпляром этого класса (в js можно сменить прототип после создания объекта). Изменения в экземпляре (не статических свойств класса) влияют только на этот конкретный экземпляр (в js изменение в прототипе повлияет на все объекты с этим прототипом)
                                        –1
                                        Вот, дошли. Я понимаю класс как
                                        «Класс — это описание пользовательского типа данных»
                                        Все.
                                        Далее пошли подуровни:
                                        1) Классы в ОО типа C++/Java
                                        2) Классы у прототипно ориентированных языков (например описанные для JS ES2015 Classes)
                                        3) Мультиметоды у других языков.
                                        4) и т.д. и т.п.
                                        Видно, из-за этого и вся путаница, что я не правильно писал «класс» вместо «описание объекта».
                                    +2
                                    что вы понимаете под словом «класс»? я лично — определенную конструкцию для описания какого-либо типа.

                                    Вот только там, что вы показали, никакого типа нет. Есть объект.

                                      0
                                      Да, я неправильно писал «класс», а нужно было «описание объекта».
                                      Признаюсь — неправильная терминология у меня.
                                        0

                                        Ура. Значит, в JS (ну, старом) классов нет. JS — ОО-язык?

                                          0
                                          Я до этого момента, думал, что спор у нас из-за того что в JS нет инструментов описания/создания объектов. Неправильно интерпретировал слово «класс».
                                          Да, классов пришедших из SmallTalk нет.
                                            +2

                                            Значит, классы не обязательны для ООП. Что возвращает нас к вопросу: почему классы в статье рассматриваются, а интерфейсы — хотя во многих случаях они лучше подходят для решения задач, описанных в статье — нет?

                                              0
                                              классы не обязательны для ООП

                                              Еще раз. Я интерпретировал, что «описание/создание объектов» не обязательны для ООП. Поэтому у нас и возникла дискуссия.

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

                                              Вам не кажется, что лучше этот вопрос задать автору статьи?
                                                +3
                                                Вам не кажется, что лучше этот вопрос задать автору статьи?

                                                Ну так с моего комментария с этим вопросом автору статьи (на который вы решили ответить) эта дискуссия и началась.

                                            0
                                            Значит, в JS (ну, старом) классов нет. JS — ОО-язык

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

                                            Со мной даже википедия согласна, что бывают недо «оопешные языки»

                                            en.wikipedia.org/wiki/List_of_object-oriented_programming_languages
                                            For example, C++ is a multi-paradigm language including object-oriented paradigm;[2] however, it is less object-oriented than some other languages such as Python[3] and Ruby.[4] Therefore, someone considers C++ as an OOP language, while others do not or prefer to name it as «semi-object-oriented programming language».

                                            Так что деление «полу/недо оопешный язык» имеет место быть.
                                              0
                                              Отсутствие наследования и прочего разрешено в ООП, но тогда его полноценным не назовешь.

                                              И вот теперь мы приходим к вопросу определения "полноценного ООП".

                              –1
                              так зачем же ОО-языку наследование?

                              Еще раз, если вам не нужно — не пользуйтесь. Я вас уверяю, даже если использовать процедурный стиль — написать можно все что угодно, чисто теоретически даже с goto, можно все засунуть в один метод.
                                0

                                Что мне нужно, я как-нибудь разберусь. Но это же вы утверждаете, что ОО-языку необходимо наследование, вот я и спрашиваю: зачем?

                                  –1
                                  Затем, чтобы его использовать в своих разработках. Это инструмент, я определяю базовую систему и собираю с помощью нее другие объекты (ну или несколько базовых систем, если у нас разрешено множественное наследование). Я прекрасно знаю способы обходится без нее. Так же я знаю способы обходится и без инкапсуляции и и прочего.
                                    0

                                    Говорят, что для "сборки систем" лучше подходит композиция. Врут, наверное?


                                    Хорошо, поставим вопрос иначе: если в языке нет наследования — он не ОО? Если да, то почему?

                                      0
                                      «Говорят, что для „сборки систем“ лучше подходит композиция. Врут, наверное?»
                                      Ну каждый решает сам, что ему удобнее
                                        0

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

                                +1
                                Тут другой вопрос, является ли Go — ООПшным языком.


                                Я же говорю: аргумент истинного шотландца.


                                Ну так сами разработчики «сомневаются»

                                golang.org/doc/faq#Is_Go_an_object-oriented_language

                                Is Go an object-oriented language?

                                Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy.

                                Но еще раз — я на эту тему холиварить не хочу.
                            0

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

                              –3
                              Вот тут не соглашусь — тот же JS, интерфейсы есть в typescript — но ИМХО — совершенно не нужная часть (В отличие от C# например)
                                0

                                … а в JS уже завезли статическую типизацию, чтобы ему интерфейсы были нужны?

                                  0
                                  Какая статическая типизация в JS? Нет там ее и не планируется, я писал о другом языке — typescript.
                                    0

                                    Тогда не приводите JS в пример. Возьмем, значит, TS. Там множественное наследование есть? Вроде как не было.

                                      0
                                      Причем тут множественное наследование? Мы говорим об интерфейсах.
                                        +1

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

                                          –1
                                          Скажем так, в языках где есть множественного наследование, интерфейсы необязательны, хотя могут присутствовать.
                                          В typescript, из-за его возможности динамической типизации (и отсутствия по понятным причинам проверки типов в рантайме), интерфейсы не обязательны. Можно прям в рантайме подставлять нужный тип (с некоторыми манипуляциями) и работать.
                                            0

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

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

                                              В данном случае «интерфейс» можно представить как класс без единой строчки кода (одни декларации функций) и protected конструктор. Это фактически народ писал руками на С++ 90х годов :)
                                              В данном случае интерфейс будет как «синтаксический сахар» для базового класса, который декларирует некоторый функционал.
                                                0
                                                Тут lair намекал, что интерфейсы это компромисс при отсутствии множественного наследования.

                                    0
                                    Вот как раз в typescript это самая интересная часть. Без них не получится правильно типизировать библиотеки которые поставляются по принципу «ядро + плагины». Например, jquery или rx.
                                      0
                                      Без них не получится правильно типизировать библиотеки

                                      Если бы библиотеки были бы переписаны на typescript, то интерфейсы не нужны были, а так это просто связующее звено между легаси.
                                        0

                                        Ну-ну, покажите как вы будете это делать. Вот представьте, что вы пишите аналог jquery на typescript и вам нужно добавить метод attr.


                                        Ну и как это делать без интерфейсов, но с сохранением выбранной архитектуры (ядро + плагины)?

                                          0
                                          но с сохранением выбранной архитектуры

                                          Вы мне предлагаете с сохранением архитектуры построенной на интерфейсах переделать на такую же без интерфейсов? Это абсурдное требование. Очевидно, что система без интерфейсов будет выглядеть совершенно по другому, но полезную работу будет выполнять одну и туже.
                                0
                                А не приведете ли Вы, как архитектор, четкое и понятное определение ООП (которого лично Вы придерживаетесь). Дабы не было разночтений и бестоковых споров.
                                  +1

                                  Я не придерживаюсь никакого четкого определения ООП, потому что я его пока не встретил. Мне ближе всего позиция Кея: "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things".

                                    0
                                    Спасибо. Жаль, ибо четкой и предметной дискуссии не получится (ибо есть только «плавающее определение» что же такое ООП). Ок :)
                        +2
                        Здесь мы используем одно из чудес, которые дает полиморфизм. Метод принимает любой снаряд. В сигнатуре указан базовый класс, а не дочерние. Но внутри метода, мы можем увидеть, что за снаряд прилетел – какого типа. И в зависимости от этого, реализуем ту или иную логику.

                        Типичный такой ад с нарушением инкапсуляции. А вот представьте, что у вас появился четвертый и пятый вид снарядов — что случится во всех классах брони?


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

                          0
                          передавали ему необходимые параметры

                          Передача параметров это не есть разве взаимодействие? Или вы тупо про инициализацию, тогда причем тут полиморфизм?
                            +2
                            Передача параметров это не есть разве взаимодействие?

                            Передавали параметры снаряду, не зная его тип. Полиморфизм (тот, который subtyping) — он именно про это.

                              0
                              Полиморфизм из ООП это как раз когда мы взаимодействуем с каким -либо типом, не зная его конкретной реализации. Т.е. когда на вход мы принимаем супертип и делаем с ним некоторые манипуляции, а теперь можем передать и его подтипы.
                                +2
                                Полиморфизм из ООП это как раз когда мы взаимодействуем с каким -либо типом, не зная его конкретной реализации.

                                Это называется "инкапсуляция".


                                Т.е. когда на вход мы принимаем супертип и делаем с ним некоторые манипуляции, а теперь можем передать и его подтипы.

                                А это называется subtyping polymorphism и не предполагает, что вы потом делаете if по типу.


                                Я, собственно, в самом начале спросил: вы понимаете, чем плох такой подход?

                                  0
                                  , что вы потом делаете if по типу.

                                  Такой код у меня 100% не пройдет ревью на работе.
                                    0

                                    Извините, случайно перепутал вас с автором поста.

                          0
                          три способа организации взаимодействия между классами

                          А вот шаблон проектирования Фабрика например, это какой способ организации?
                          Конечно их больше, а ваш код следует улучшать дальше, к примеру классы брони — обычная копипаста, добавление новой брони усложнено, а при изменении интерфейса класса нужно делать много работы. Эта проблема может быть решена как мета-программированием, так и в рамках ООП.
                            –1
                            Для меня ключевое понимание наследования и отличие его от композиции пришло после понимания механизма их работы. Приведу пример. Допустим у нас есть класс DBConnection объект которого представляет собой соединение с базой данных и базовые операции работы с базой. Применяя композицию обычно создают отдельный класс Repository который представляет собой crud-операции с базой, который в конструкторе создает объект соединения и сохраняет его каком-то поле и использует потом его для взаимодействия c базой данных в crud-методах. А вот применяя наследование вместо композиции класс Repository отнаследуется от DBConnection и добавит нужный код работы с crud. И здесь принципиальное отличие — в случае композиции при создании объекта Repository будет создано два объекта в рантайме (сам Repository и объект DBConnection) а применяя наследование — только один объект. А в случае если у нас будет цепочка из 10 различных сущностей которые что-то добавляют и переопределяют то с композицией это будет уже 10 объектов а с наследованием только один вне зависимости от длины этой цепочки (да хоть тысячу сущностей). В этом и суть наследования — оно позволяет вынести в compile-time декораторную логику экономя cpu-циклы и память
                              +1
                              Здесь мы используем одно из чудес, которые дает полиморфизм. Метод принимает любой снаряд. В сигнатуре указан базовый класс, а не дочерние. Но внутри метода, мы можем увидеть, что за снаряд прилетел – какого типа.


                              Вот проверки на тип ниразу не полиморфизм, имхо. С таким подходом можно все методы на Object завязать, вот уж будет полиморфизм.
                                +1
                                A «owns» B = Composition: B has no meaning or purpose in the system without A
                                A «uses» B = Aggregation: B exists independently (conceptually) from A
                                  –1
                                  насколько это всё не нужные ограничения, показывает следующий пример стирания типов без вредных эффектов.
                                  Пусть код должен поддерживать разные виды танков бронетехники, колёсные и гусеничные, с разным количеством пушек, моторов и т.п.
                                  пусть просто класс Tank имеет Map<TypeID, List>, не Object, чтобы туда дураки не кидали любые обьекты. TankPart имеет integer или enum поле TypeID уникальную для каждого типа или может быть заменено на Class смотря что в каком языке есть. При добавлении новой детали проводится довольно простая и очевидная операция:
                                  map.get(part.typeId).add(part).
                                  Интересно, особо внимательные заметили что описанное выше — питоний стиль??
                                  с танком можно тогда делать более умные вещи такие как приказать стрелять из всех, либо из определённого числа орудий, либо разделить орудия в этой Map по более мелким классам, либо одни и те же детали добавить в разные классы чтоб вызывать для разных дел, причём это всё можно динамически менять с малым оверхедом на содержание карты.
                                    +1

                                    … и как бишь в этой конструкции определить, что именно умеет конкретный TankPart — стрелять или ездить?

                                      –2
                                      по typeID. Если не хочешь запоминать соответствие число -> что часть умеет, есть старый но проверенный способ это битовые флаги
                                        +2

                                        Т.е. потребитель должен знать список всех возможных TypeID, а поставщик — не забывать его обновлять, а если не дай бог поведение поменялось, то отследить это можно только тестами?

                                          0
                                          В общем случае — да.
                                          Не найдет ожидаемого в 'Run time' — сгенерится ошибка типа «NULL Reference exception» / «Application Exception» и т.д. в зависмости от архитектуры
                                            0

                                            Ну и зачем нам такое счастье без особых на него причин?

                                              0
                                              Банально — экономия ресурсов, поскольку в RunTime ничего не проверяется.
                                              Все проверяется на этапе компиляции статическими анализаторами (в лучшем случае).
                                              Например, COM с такими спецэффектами работал, если руками править IDL не понимая всех последствий :)
                                              Если еще древнее и нагляднее — динамическая загрузка DLL и ручной поиск указателей на функции :) В Pascal overlay механизм.
                                              Это сейчас ресурсов дофига — и памяти и процессора, а раньше это был дефицит. Выравнивали побайтово, что бы поменьше места занимало.
                                                0
                                                Банально — экономия ресурсов, поскольку в RunTime ничего не проверяется.

                                                Вы думаете, при вызове несуществующего метода ничего не проверяется? Не говоря уже о том, что в C# — о котором идет речь в статье — это немножко невозможно без дополнительных прыжков?

                                                  –1
                                                  Вы думаете, при вызове несуществующего метода ничего не проверяется?

                                                  Я не думаю, я знаю. При не корректном указателе — будет попытка исполнения не корректной операции процессора или обращения по некорректному указателю.
                                                  Дальше ОС отловит ошибку и прибьет процесс. Насмотрелся :)
                                                  Например: исполнить код в области данных. Сейчас такие операции контролируются на уровне ОС).

                                                  Не говоря уже о том, что в C# — о котором идет речь в статье — это немножко невозможно без дополнительных прыжков?

                                                  Вопрос был другой и С# не касался.
                                                  А насчет С# есть стандарт:
                                                  New types—value types and reference types—are introduced into the CTS via type declarations expressed in metadata. In addition, metadata is a structured way to represent all information that the CLI uses to locate and load classes, lay out instances in memory, resolve method invocations, translate CIL to native code, enforce security, and set up runtime context boundaries.
                                                    +1
                                                    Например: исполнить код в области данных. Сейчас такие операции контролируются на уровне ОС

                                                    Это ваше "контролируется на уровне ОС" — это не проверка, по-вашему?


                                                    Вопрос был другой и С# не касался.
                                                    Да нет, вопрос был в контексте поста, а пост — про C#.
                                                      0
                                                      Это ваше «контролируется на уровне ОС» — это не проверка, по-вашему?

                                                      Это все таки не зависит от языка или среды исполнения. Одинаково будет «ловить ошибки» что на С++ что на Java или .Net

                                                      К примеру:
                                                      Overview of the Protected Mode Operation of the Intel Architecture

                                                      If we look back at the segment descriptor you will see information in the descriptor that relates to more than just its base address in memory (Figure 2 & Table 1). The additional information provided is primarily for the implementation of a protected system:
                                                      • How programs can access different types of segments,
                                                      • ensuring accesses within the limits of the segment (limit checking),
                                                      • maintaining privilege levels or who has access to a segment,
                                                      • and controlling access to privileged instructions.


                                                      Возвращаясь к С#
                                                      Т.е. потребитель должен знать список всех возможных TypeID, а поставщик — не забывать его обновлять, а если не дай бог поведение поменялось, то отследить это можно только тестами?

                                                      Следует ответ «Да должен знать всегда», поскольку стандарт говорит следующее:
                                                      Signatures are the part of a contract that can be checked and automatically enforced. Signatures are formed by adding constraints to types and other signatures. A constraint is a limitation on the use of or allowed operations on a value or location. Example constraints would be whether a location can be overwritten with a different value or whether a value can ever be changed.
                                                      All locations have signatures, as do all values.

                                                      Type safety and verification
                                                      Since types specify contracts, it is important to know whether a given implementation lives up to these contracts. An implementation that lives up to the enforceable part of the contract (the named signatures) is said
                                                      to be type-safe. An important part of the contract deals with restrictions on the visibility and accessibility of named items as well as the mapping of names to implementations and locations in memory.
                                                      Type-safe implementations only store values described by a type signature in a location that is assignment-compatible (§8.7) with the location signature of the location (see §8.6.1).
                                                      Type-safe implementations never apply an operation to a value that is not defined by the exact type of the value. Type-safe implementations only access locations that are both visible and accessible to them. In a type-safe implementation, the exact type of a value cannot change.
                                                      Verification is a mechanical process of examining an implementation and asserting that it is type-safe.
                                                      Verification is said to succeed if the process proves that an implementation is type-safe. Verification is said to fail if that process does not prove the type safety of an implementation. Verification is necessarily conservative:
                                                      it can report failure for a type-safe implementation, but it never reports success for an implementation that is not type-safe.
                                                      For example, most verification processes report implementations that do pointer-based arithmetic as failing verification, even if the implementation is, in fact, type-safe.

                                                        0
                                                        Это все таки не зависит от языка или среды исполнения.

                                                        ОС — это "среда исполнения". Так что зависит. И даже процессор — это "среда исполнения".


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


                                                        Следует ответ «Да должен знать всегда», поскольку стандарт говорит следующее:

                                                        А какое отношение этот стандарт имеет к самописным идентификаторам?


                                                        (более того, даже этот стандарт не говорит, что потребитель должен знать список всех типов, которые могут в него передать — только те ограничения, которые он накладывает на принимаемые типы)

                                                          0
                                                          Но суть все равно не в этом, а в том, что в современных реалиях попытка вызвать несуществующий метод небесплатна: именно вследствие дополнительных проверок, защищающих систему от сбоев.

                                                          В общем проверка бесплатна, поскольку аппаратная и уже есть.
                                                          Срабатывает один раз, а не постоянно.
                                                          Скажем так — это неизбежное «константное» зло :) Скорее всего, обработка ошибок такого типа будет значительно дороже.

                                                          А какое отношение этот стандарт имеет к самописным идентификаторам?

                                                          Это в общем-то идентификаторы типов .Net.
                                                          И да, они самописны… компилятором :)

                                                          (более того, даже этот стандарт не говорит, что потребитель должен знать список всех типов, которые могут в него передать — только те ограничения, которые он накладывает на принимаемые типы)

                                                          И тут Вы правы, ибо этот же стандарт отдает реализацию на усмотрение разработчика :)


                                                          The choice of a particular verification process is thus a matter of engineering, based on the resources available to make the decision and the importance of detecting the type safety of different programming constructs.

                                                          Например:
                                                          When a class is loaded at runtime, the CLI loader imports the metadata into its own in-memory data structures, which can be browsed via the CLI Reflection services. The Reflection services should be considered as similar to a compiler; they automatically walk the inheritance hierarchy to obtain information about inherited methods and fields, they have rules about hiding by name or name-and-signature, rules about inheritance of methods and
                                                          properties, and so forth.
                                                            0
                                                            В общем проверка бесплатна, поскольку аппаратная и уже есть.

                                                            Эээ… нет же. Не бесплатна.


                                                            Срабатывает один раз, а не постоянно.

                                                            На каждом вызове же.


                                                            Это в общем-то идентификаторы типов .Net.

                                                            То, что предлагают в комментарии в начале треда? Нет, это не они.


                                                            А если заменить то, что там предлагается, на нормальные типы .net, то мы получим нормальную статически типизированную систему.

                                                              –1
                                                              В общем проверка бесплатна, поскольку аппаратная и уже есть.

                                                              Эээ… нет же. Не бесплатна.

                                                              Транзисторы есть и с этой точки зрения да «уплочено». Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?
                                                              Срабатывает один раз, а не постоянно

                                                              На каждом вызове же.

                                                              Если на первом вызове выяснится, что не корректный адрес функции и приложение попробует вызвать эту функцию, то приложение будет «ломиться до посинения»? Или ОС просто прибьет не корректное приложение и на этом все закончится?
                                                              Да и после формирования пространства процесса (выделение и распределения памяти, загрузки кода и данных) ОС не занимается перетасовкой адресов функций «потому что скучно», это ответсвенность самого процесса, в рамках предоставленых прав «сегмент кода»/«сегмент данных».
                                                              Если, конечно не задаться целью наваять такое «забавное» приложение.
                                                                0
                                                                Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?

                                                                По сравнению с отсутствием этих расчетов — конечно, влияет.


                                                                Если на первом вызове выяснится, что не корректный адрес функции и приложение попробует вызвать эту функцию, то приложение будет «ломиться до посинения»? Или ОС просто прибьет не корректное приложение и на этом все закончится?

                                                                А если адрес корректный, сколько раз будет выполняться проверка?


                                                                Давайте с другой стороны посмотрим. Вот исходное предложение:


                                                                Пусть код должен поддерживать разные виды танков бронетехники, колёсные и гусеничные, с разным количеством пушек, моторов и т.п.
                                                                пусть просто класс Tank имеет Map<TypeID, List>, не Object, чтобы туда дураки не кидали любые обьекты. TankPart имеет integer или enum поле TypeID уникальную для каждого типа

                                                                Очевидно, что каждый TankPart имеет разные операции (пушка — стреляет, фара — светит, мотор — крутится, и так далее). Предположим, что нам надо сказать "включить все фары". Как это сделать в рамках исходного предложения, и чем это выгоднее стандартного ICollection<Light>? Напомню, что контекст — C#, .net.

                                                                  0
                                                                  Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?

                                                                  По сравнению с отсутствием этих расчетов — конечно, влияет.

                                                                  Да в общем-то нет. Как было X операций в N единиц времени, так и будет. Эти битовые маски расчитываются и контролируются аппаратно процессором в процессе исполнения комманд.

                                                                  Например согласно "“Protection” руководства “Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A & 3B):System Programming Guide”

                                                                  Процессор использует эту информаци. для детектирования программных ошибок результатом которых является попытка использования сегмента или шлюза (gate) неверным или неожиданным способом.

                                                                  Следующий список дает примеры типичных операций в которых выполняется проверка типов (список не исчерпывающий):

                                                                  В процессе определенных внутренних операций,
                                                                  При вызове или переходе через call gate (или при прерывании или обработчике исключения через trap или interrupt gate) процессор автоматически проверяет что сегментный дескриптор указанный в gate является кодовым сегментом.
                                                                  Когда операнд инструкции содержит селектор сегмента
                                                                  определенным инструкциям разрешен доступ к сегментам или шлюзам (gates) только определенного типа, например:
                                                                  Дальнему вызову CALL или инструкции JMP разрешен доступ к сегментному дескриптору содержащему «conforming code segment», «nonconforming code segment», call gate, task gate или TSS.

                                                                  и
                                                                  Program-Error Exceptions
                                                                  Процессор генерирует одно или более исключений при обнаружении программных ошибок в процессе выполнении приложения, кода операционной системы или executive. Архитектуры Intel64 и IA-32 определяют vector number для каждого processor-detectable exception.
                                                                  Исключения подразделяются на faults, traps и aborts.

                                                                  Программно-генерируемые исключения
                                                                  Инструкции INTO, INT3 и BOUND позволяют программную генерацию исключений. Эти инструкции позволяют выполнение проверки условий в местах выполнения потока инструкций. Например INT 3 вызывает генерацию breakpoint exception.

                                                                  Machine-Check Exceptions
                                                                  Процессоры семейств P6 family и Pentium предоставляют внутренние и внешние machine-check механизмы для проверки операций внутреннего аппаратного чипа и транзакций шины. Эти механизмы — implementation dependent (непереносимы). Когда процессор обнаруживает machinecheck
                                                                  ошибку, процессор сигнализирует об ошибке с помощью machine-check exception (vector 18) и возвращает код ошибки.


                                                                  Что касается «Предположим, что нам надо сказать „включить все фары“. Как это сделать в рамках исходного предложения, и чем это выгоднее стандартного ICollection? Напомню, что контекст — C#, .net.»
                                                                  Ответ прост — кто-то должен будет сделать эту работу по выяснению наличия возможности «включить».
                                                                  Например:
                                                                  if (unit is IOnOff)
                                                                  {
                                                                  ((IOnOff)unit).OFF();
                                                                  }
                                                                  else
                                                                  {
                                                                  throw new Exception();
                                                                  }

                                                                  или
                                                                  *p->Off();

                                                                  Или это сделает программист руками, или ОС внутреними функциями или CPU аппаратными возможостями. Просто обработка ошибок более дорогое удовольствие, чем нормальны код.
                                                                    0
                                                                    *p->Off();

                                                                    Я что-то не думаю, что в C# можно так написать. Фиг с ним, с указателем, но вам же надо знать, что такое Off, а вы этого не сделаете без операции приведения типа.


                                                                    Так что ответа на мой вопрос я так и не вижу.


                                                                    И это не говоря о том, что сравниваю я с вариантом ICollection<ITankLight>.ForEach(l => l.Off()), в котором как раз операций приведения нет.

                                                                      –1
                                                                      Я что-то не думаю, что в C# можно так написать. Фиг с ним, с указателем, но вам же надо знать, что такое Off, а вы этого не сделаете без операции приведения типа.

                                                                      Да ладно ?! :)
                                                                      using System;

                                                                      struct Point
                                                                      {
                                                                      public int x;
                                                                      public int y;

                                                                      public override string ToString() {
                                                                      return "(" + x + "," + y + ")";
                                                                      }
                                                                      }

                                                                      class Test
                                                                      {
                                                                      static void Main() {
                                                                      Point point;
                                                                      unsafe {
                                                                      Point* p = &point;
                                                                      p->x = 10;
                                                                      p->y = 20;
                                                                      Console.WriteLine(p->ToString());
                                                                      }
                                                                      }
                                                                      }

                                                                      в котором как раз операций приведения нет.

                                                                      Проверка на усмотрение разрабочика и среды исполнения. Но, полагаю некоторые механизмы оптимизации должны быть. Глупо постоянно проверять неизменяемые вещи. Проще проверить на этапе компиляции и/или загрузки сборки.
                                                                        0
                                                                        Да ладно ?! :)

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


                                                                        Проверка на усмотрение разрабочика и среды исполнения.

                                                                        Зачем там какая-либо проверка, если там все типы явные?


                                                                        Проще проверить на этапе компиляции и/или загрузки сборки.

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

                                                                          –1
                                                                          Не, не ладно. Вы используете указатель того же типа, что и присваемое ему значение, и вызываете метод, который есть у этого типа. Это совсем не то, что нужно в описанном сценарии.

                                                                          Нет. В этом и нюанс.
                                                                          Вы декларируете в runtime, то это указатель типа Х. Cреда исполения доблестно дергает функцию по адресу A, а там все что угодно. Хотя бы и функция с другой сигнатурой, приведет к не корректному заполнению стека вызова функции.
                                                                          Типичные грабли цепочки приведения типов:
                                                                          *typeX -> *void ->*typeY.
                                                                          После дергаем *typeY->FnY() и получаем «грабли в лоб».
                                                                          Проверка на усмотрение разрабочика и среды исполнения.

                                                                          Зачем там какая-либо проверка, если там все типы явные?

                                                                          Типовые проблемы в runtime: не корректный вычисленный адрес или размер данных, адрес функции.
                                                                          Например переполнение стека, выход за границы массива, нулевой указатель.
                                                                            +1
                                                                            Вы декларируете в runtime, то это указатель типа Х. Cреда исполения доблестно дергает функцию по адресу A, а там все что угодно.

                                                                            Вы код-то на C# покажите, который это делает. Мне вот не удалось даже уговорить C# сделать указатель на интерфейс или класс.


                                                                            (и это мы еще не трогаем того факта, что просто не надо использовать unsafe для таких вещей)


                                                                            Типовые проблемы в runtime: не корректный вычисленный адрес или размер данных, адрес функции.
                                                                            Например переполнение стека, выход за границы массива, нулевой указатель.

                                                                            Эти "типовый проблемы" не имеют отношения к дискуссии (и все проверяются .net, btw).

                                                                              –2
                                                                              Привет «грабли», которые отлично компилируются:
                                                                              class TypeX
                                                                              {
                                                                              public void FnX()
                                                                              {
                                                                              return;
                                                                              }
                                                                              }

                                                                              class TypeY
                                                                              {
                                                                              public void FnY()
                                                                              {
                                                                              return;
                                                                              }
                                                                              }

                                                                              class Program
                                                                              {
                                                                              static void Main(string[] args)
                                                                              {

                                                                              TypeX instX = new TypeX();
                                                                              instX.FnX();

                                                                              ((TypeY)((object)instX)).FnY();

                                                                              }
                                                                              }

                                                                              А в результате отлично «грабли прилетают в лоб» в runtime: System.InvalidCastException: 'Unable to cast object of type 'ConsoleTest.TypeX' to type 'ConsoleTest.TypeY'.'
                                                                                +1

                                                                                Ну так я выше и написал, что вам не удастся получить такого эффекта без явного приведения типа. Вы только что продемонстрировали, что приведение типа нужно (а внутри него у .net есть проверка, которая и защищает нас от совсем непредсказуемого поведения). Что, как бы, демонстрирует нам, что в рамках C# подход из стартового комментария не дает вам более дешевого, как вы говорили выше, решения, потому что проверок в нем больше, чем в стандартном решении со статической типизацией.


                                                                                QED.

                                                                                  0

                                                                                  BTW, вызова метода у вас не получилось, у вас упало-то ощутимо раньше.

                                                                                    –1
                                                                                    Исходя из декларации назначения механизмов защиты упоминаемых выше, их назначение в уменьшение ущерба в случае человеческих ошибок, намеренных действий или аппартных ошибок.
                                                                                    Стратегия строится на как можно раннем этапе предотвращения ошибок такими методами:
                                                                                    • язык разработки
                                                                                    • инструменты анализа
                                                                                    • внедрение «защитного» кода на этапе компиляции (example: Check for arithmetic overflow/underflow, Buffer Security Check, Run-Time Error Checks) & etc
                                                                                    • аппаратные, как последняя линия обороны
                                                                                    в стандартном решении со статической типизацией.

                                                                                    Это превентивный мехнизм, но он не помогает в процессе исполнения. В Runtime приходится применять другие механизмы. .Net предлагает свои механизмы, Java свои. А С++ например практически таких механизмов не предлагает и полагается в этом вопросе на разработчика.
                                                                                    BTW, вызова метода у вас не получилось, у вас упало-то ощутимо раньше.

                                                                                    Да. Это сработал один из защитных механизмов .Net в Runtime (проверил описание типов и цепочки наследования, согласно приведенным выше цитатам из стандартов для .Net). А на уровне спецификации языка «Object references are not blittable.» и соответсвующая ошибк компилятора: error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type
                                                                                    Для С++ будет ошибка типа «Unhandled exception in BlaBlaBla.exe 0xC0000005 access violation» или «Unhandled exception at 0x7715A849 (ntdll.dll) in BlaBlaBla.exe: 0xC0000374: A heap has been corrupted (parameters: 0x77195910).» или еще черти что выпадет.
                                                                                    Сейчас с этим значительно лучше стало — средства разработки улучшились и «поумнели»
                                                                                      0
                                                                                      Исходя из декларации назначения механизмов защиты упоминаемых выше, их назначение в уменьшение ущерба в случае человеческих ошибок, намеренных действий или аппартных ошибок.

                                                                                      Эээ… там нет механизмов защиты, ровно наоборот. Если вы про исходный коммент, конечно, а не про что-то свое.


                                                                                      Это превентивный мехнизм, но он не помогает в процессе исполнения.

                                                                                      Он помогает уменьшить количество проверок на этапе исполнения.

                                                                                        0
                                                                                        Эээ… там нет механизмов защиты, ровно наоборот. Если вы про исходный коммент, конечно, а не про что-то свое.

                                                                                        Не вижу смысла оспаривать документацию по .Net CLI VM:

                                                                                        The verification algorithm shall attempt to associate a valid stack state with every CIL instruction. The stack state specifies the number of slots on the CIL stack at that point in the code and for each slot a required type that shall be present in that slot.

                                                                                        The verification algorithm shall simulate all possible control flow paths through the code and ensure that a valid stack state exists for every reachable CIL instruction.

                                                                                        Verification simulates the operation of each CIL instruction to compute the new stack state, and any type mismatch between the specified conditions on the stack state and the simulated stack state shall cause the verification algorithm to fail.

                                                                                        The VES ensures that both special constraints and type constraints are satisfied. The constraints can be checked
                                                                                        as early as when a closed type is constructed, or as late as when a method on the constrained generic type is invoked, a constrained generic method is invoked, a field in a constrained generic type is accessed, or an instance of a constrained generic type is created.

                                                                                        A CIL instruction can throw a range of exceptions. The CLI can also throw the general purpose exception called ExecutionEngineException.


                                                                                        и т.д. Там много такого люботыного.

                                                                                        Например открываем документацию для С++ и читаем:
                                                                                        Buffer Security Check

                                                                                        Security Checks

                                                                                        On functions that the compiler recognizes as subject to buffer overrun problems, the compiler allocates space on the stack before the return address. On function entry, the allocated space is loaded with a security cookie that is computed once at module load. On function exit, and during frame unwinding on 64-bit operating systems, a helper function is called to make sure that the value of the cookie is still the same. A different value indicates that an overwrite of the stack may have occurred. If a different value is detected, the process is terminated.


                                                                                        если это все называется "… там нет механизмов защиты ..." боюсь тогда представить тогда что такое "… там есть механизмы защиты ..." :)

                                                                                        p.s.
                                                                                        Предлагаю пока закругляться с этим вопросом и возможно вернуться к нему позже :)
                                                                                          0
                                                                                          Не вижу смысла оспаривать документацию по .Net CLI VM:

                                                                                          Я же говорю: вы о чем-то своем. В исходном комментарии предлагается положить болт на статическую типизацию, которую приносит .net, и использовать самописный костыль с придуманным TypeId. И это, как мне кажется, никаких достоинств, кроме NIH, не имеет.

                                                                                            0
                                                                                            А зачем тогда придумали «To fully identify a type, the type name shall be qualified by the scope that includes the type name.»?
                                                                                            Опять же, зачем то сделали:
                                                                                            Type signatures
                                                                                            Type signatures define the constraints on a value and its usage. A type, by itself, is a valid type signature. The type signature of a value cannot be determined by examining the value or even by knowing the class type of the value. The type signature of a value is derived from the location signature (see below) of the location from which the value is loaded or from the operation that computes it. Normally the type signature of a value is the type in the location signature from which the value is loaded.

                                                                                            Не вижу проблем, при загрузке или компиляции генерить уникальные идентификаторы/хеши типов для быстрой проверки. Не парсить же описание типов каждый раз при каждом обращении.
                                                                                            Возможно Type.GUID Property уже служит для этих оптимизаций.
                                                                                            COM такой механизм опознавания типов использовал.
                                                                                            Это не новая идея и уже была реализована.
                                                                                              0
                                                                                              А зачем тогда придумали «To fully identify a type, the type name shall be qualified by the scope that includes the type name.»?

                                                                                              Не знаю.


                                                                                              Не вижу проблем, при загрузке или компиляции генерить уникальные идентификаторы/хеши типов для быстрой проверки. Не парсить же описание типов каждый раз при каждом обращении.
                                                                                              Возможно Type.GUID Property уже служит для этих оптимизаций.

                                                                                              Это все было бы круто, если бы потом не надо было делать каст, внутри которого .net все равно сделает проверку типа.

                                                                                                0
                                                                                                Не знаю.

                                                                                                Нашел таки описание в стандарте:
                                                                                                Externally, an assembly is a collection of exported resources, including types. Resources are exported by name.

                                                                                                The identity of a type is its assembly scope and its declared name. A type defined identically in two different assemblies is considered two different types.

                                                                                                New types—value types and reference types—are introduced into the CTS via type declarations expressed in metadata. In addition, metadata is a structured way to represent all information that the CLI uses to locate and load classes, lay out instances in memory, resolve method invocations, translate CIL to native code, enforce security, and set up runtime context boundaries. Every CLI PE/COFF module (see Partition II Metadata – File Format) carries a compact metadata binary
                                                                                                that is emitted into the module by the CLI-enabled development tool or compiler.

                                                                                                Each CLI component carries the metadata for declarations, implementations, and references
                                                                                                specific to that component. Therefore, the component-specific metadata is referred to as component metadata, and the resulting component is said to be self-describing. In object models such as COM or CORBA, this information is represented by a combination of typelibs, IDL files, DLLRegisterServer, and a myriad of custom files in disparate formats and separate from the actual executable file. In contrast, the metadata is a fundamental part of a CLI component.

                                                                                                When a class is loaded at runtime, the CLI loader imports the metadata into its own in-memory data structures, which can be browsed via the CLI Reflection services. The Reflection services should be considered as similar to a compiler; they automatically walk the inheritance hierarchy to obtain information about inherited methods and fields, they have rules about hiding by name or name-and-signature, rules about inheritance of methods and properties, and so forth.

                                                                                                A metadata token is an implementation-dependent encoding mechanism. Partition II describes the manner in which metadata tokens are embedded in various sections of a CLI PE/COFF module. Metadata tokens are embedded in CIL and native code to encode method invocations and field accesses at call sites; the token is used by various infrastructure services to retrieve information from metadata about the reference and the type on which it was scoped in order to resolve the reference.
                                                                                                A metadata token is a typed identifier of a metadata object (such as type declaration and member declaration). Given a token, its type can be determined and it is possible to retrieve the specific metadata attributes for that metadata object. However, a metadata token is not a persistent identifier. Rather it is scoped to a specific metadata binary. A metadata token is represented as an index into a metadata data structure, so access is fast and direct.



                                                                                                Глянул в 'WebAssembly Core Specification':
                                                                                                5. Binary Format
                                                                                                5.3. Types
                                                                                                5.3.1. Value Types

                                                                                                Value types are encoded by a single byte.
                                                                                                ​valtype​::=​0x7F => i32
                                                                                                valtype​::=​0x7E => i64
                                                                                                valtype​::=​0x7D => f32
                                                                                                valtype​::=​0x7C => f64


                                                                                                Так что насчет "… должен знать типы… если поменялось то обнаружить только тестами..." и тут ответ похоже «да должен знать» и «да только тестами проверить»

                                                                                                Это все было бы круто, если бы потом не надо было делать каст, внутри которого .net все равно сделает проверку типа.

                                                                                                Тут согласен — не очень рационально использовать велосипед вместо уже сделанного и отпимизированного.
                                                                                                  0
                                                                                                  Так что насчет "… должен знать типы… если поменялось то обнаружить только тестами..." и тут ответ похоже «да должен знать» и «да только тестами проверить»

                                                                                                  Для прикладного кода под .net это, очевидно (и демонстрируемо), не так.

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

                                    Самое читаемое