Pull to refresh

Comments 45

Ох уж эта запятая. Успехов в дальнейшем!
Вдохновляющая история)
Хороший разбор с первопричинами дает более ценный опыт.
Навскидку, конечно, со многим можно не согласиться, но как нибудь в другой раз)

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

То есть
class MyClassA{
    private int a;
    public void DoIt(int b, int c){...}
}

На самом деле
class MyClassA{
    private int a;
    private static void DoIt(MyClassA this, int b, int c){...}
}


А еще мое имхо — самые мощные средства связаны с полиморфизмом (хотя это и не свойство ООП) и с сахаром, предназначенным для сокрытия.

разве статический метод имеет доступ к нестатическим полям класса?

Статический метод в данной ситуации принимает экземпляр класса в первом параметре, о том и речь.
Более того если взять конкретно C# и посмотреть на extension methods, то будет видно что они именно так и работают и имеют идентичный синтаксис.
Заголовок спойлера
image

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

Статические методы имеют доступ только к статическим же полям, проперти и атрибутам.
Речь про то, что в картинке под спойлером в сообщении выше. Статический метод класса MyClass.DoItStatic(MyClass item, int k) принимает аргумент item класса MyClass и залезает в его приватные поля (в картинке тест этой гипотезы в Visual Studio, все работает, код запускается и присваивает значение этому полю).
Статический метод любого другого класса, принимающий этот же объект, в его приватные поля уже залезть не может.
Аааа, вы в этом плане. Да, сразу не увидел. Есть такая фишка. Но это не только в C# так. Просто есть языки с «class-level access modifiers» и есть с «object-level access modifiers».
Вообще не имеет, но тут ведь экземпляр класса передается первым параметром.
Это псевдокод — иллюстрация.
В том и обяснение — нестатический метод первым параметром получает ссылку на сам класс — и именно поэтому имеет к ней доступ.

То есть ООП — это просто способ организации + сахар

Когда ты делаешь
var a = new MyClassA();
a.DoIt(1, 2);


Компилятор превращает в
var a = new MyClassA();
MyClassA.DoItStatic(a, 1, 2);


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

В Python, кстати, self переменная задается явно
Это объяснение, что принцип такой, я не предлагаю так делать.
Абстрактно можно считать методы — простыми функциями, в которые вместе с параметрами передается ссылка на объект, от имени которого вызывается объект.
То есть вообще — речь о том, что ООП — это просто абстракция, сахар, нужная для организации. После компиляции по факту он превращается в обычный процедурный код. Соответственно ООП может быть сэмитирована через обычный процедурный код (привет С++ и линукс, написанный на С в объектном стиле)
Область видимости — такой же сахар, нужный чтобы ограничить программиста в использовании класса. Оно работает только до компиляции, чтобы сообщить «автор класса дал тебе публичный интерфейс — пользуйся им, чтобы работать с объектом безопасно». Приватные переменные — внутреннее состояние объекта, которое изолируется и защищается.
Это позволяет собственно делать самое полезное, что есть в ООП — разбивать код на понятные блоки, каждый из которых на своем уровне работает с простыми снаружи кусочками, сложными внутри.

Кстати статическому классу доступны приватные поля его класса через ссылку:
    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            a.DoIt(1);
            A.DoItStatic(a, 2);
        }
    }

    public class A {
        private int b = 1;

        public void DoIt(int c) {
            b = c;
            Console.WriteLine(b);
        }

        public static void DoItStatic(A @this, int c) {
            @this.b = c;
            Console.WriteLine(@this.b);
        }
    }

Вот так работает
если так рассуждать, то и сам код на С это абстракция над машинными кодами…
Именно так. А машинный код это в свою очередь абстракция над физическими процессами :)
Разумеется
Но для понимания некоторых черных ящиков полезно понимать, на каких костылях они стоят.
Если учить от обратного (с высокоуровневых вещей), такого понимания не будет и магия будет больше магией.

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

"Научиться ООП" можно кучей разных способов.


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


И когда у вас большой проект, да ещё скажем над ним работают куча людей, да ещё и разбросанных территориально… Вот тогда начинаешь замечать что без всего этого "сахара" становится сложно.

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

Основное преимущество ООП перед процедурным программированием, пожалуй, — всё же полиморфизм, т.е. возможность функцией с одним и тем же именем делать разные вещи в зависимости от типа аргумента. Он нужен вовсе не только при строгой или статической типизации. В том же Матлабе (или в обычной математике) умножение — вполне себе полиморфная функция. В Common Lisp Object System, например, ООП понимается как возможность написания функций, поведение которых зависит от (рантайм) типов аргументов (и немножко есть наследование, но за… дцать лет все уже поняли, что наследование, кроме интерфейсов, почти нигде не нужно).


Инкапсуляцию тоже сейчас часто рассматривают не с точки зрения сокрытия полей/свойств/методов, а с точки зрения объединения комплекса взаимозависимых операций в рамках одного метода (в Python, например, сокрытия нет). В этом смысле это не совсем ООП-шная штука, но можно, с другой стороны, сказать, что даже когда в процедурном языке мы пишем структуры, определяем несколько интерфейсных процедур для них и дальше работаем только через этот интерфейс — это уже элемент ОО проектирования.

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

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


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

На процедурных языках программирования можно легко писать в ООП стиле.
Собственно части Линукса так написаны — на С, но в процедурной парадигме, с оформленными специально объектами (структуры + обрабатывающие их методы).
И С++ изначально был просто препроцессором для С, то есть по факту сахаром, который спокойно переводился в С код.
Инкапсуляция — сахар. Просто синтаксис для удобного связывания, и организации.
Области видимости — сахар. Аналог наследование может быть достигнут включениями или побайтовым совпадением структур (чтобы их подкладывать)
И только аналоги интерфейсов и виртуальных методов для полиморфизма реализуются сравнительно тяжело, с помощью ссылок на ссылки на функции и в этом духе.
ООП — реально про организацию кода и ООП языки — просто специально адаптированные под нужный сахар.
Так то мое ИМХО, что rust и go — вполне себе ООП френдли, потому что единственное, чего там нет, это наследования реализации (от которых и так рекомендуют отказываться все кому не лень), все остальные пункты — вполне.
Основное преимущество ООП перед процедурным программированием, пожалуй, — всё же полиморфизм

То, какие преимущества даёт или не даёт ООП лично вам, зависит в первую очередь от ваших задач и от ваших проблем. Кому-то полиморфизм вообще не интересен. А кому-то ООП создаст больше проблем чем решит.
Т.е. там возникают лишние проверки? Или на этапе компиляции компилятор может оценить, что на вход приходит только один тип и выкинуть проверки из кода?

В CLOS? Насколько мне известно, да, лишние проверки во время выполнения. Теоретически, стандарт не запрещает диспетчеризацию при компиляции, но на практике, кажется, нет таких реализаций. Но есть умельцы, которые не хотят с этим мириться.

Я про ООП в общем случае.

Общего случая скорее нет. Важно то, что семантически появляется понятие "типа времени исполнения", в зависимости от которого выражение x.f() будет приводить к выполнению разных наборов инструкций. Как это реализовано — второстепенный вопрос.


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


В Python объекты — это словари, в них лежат как данные, так и ссылки на методы. Там, вроде бы, для поиска метода тип объекта не требуется, но поиск идёт уже в рантайме.

А есть какие-то задачи, где можно заботать ООП на примерах, типа какого-нибудь codewars, т.е. чтоб было понятно как и зачем применять ООП вместо обычного кода?

Задач не помню (но я и не фанат ООП, скорее тяготею к ФП). В "Алгоритмах" Седжвика примеры структур данных, куда ООП хорошо ложится.


Например, написать функцию общего вида для поиска корней на отрезке методом половинного деления.


Если язык не поддерживает first-class functions, то можно сделать интерфейс Callable, который требует наличия метода call, и бисекция пишется в стиле:


float find_root(Callable f, float a, float b) {
    float fa = f.call(a);
    float fb = f.call(b);
    while (abs(b - a) < 1e-5) {
    ...
    }
    return answer;
}

Сами объекты, реализующие интерфейс Callable, могут быть совершенно произвольными.


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

Много ООП при написании всяких GUI-приложений.
А так, в не-ГУИ приложениях — например, решение нелинейных дифуравнений методом Рунге-Кутты.
Если уравнение приводимо в явном виде к такой форме
dy/dx=f(x,y),
то пишете базу данных EqSolver, куда пихаете все коэффициенты, входящие в функцию f(x,y).
Далее
EqSolver solver=new EqSolver()
solver.coeff1=…
solver.coeff2=…
… так делаете все коэффициенты
1) У вас будет метод f=solver.f(x,y), внутри он будет обращаться к coeff1, coeff2… и возвращать значение
2) У вас будет универсальный метод расчета 1й итерации который вычисляет значение функции f как f=this.f(x,y) вместо f=f(x,y,coeff1,coeff2,,,,)
3) У вас будет универсальный метод, который вызывает в цикле метод расчета одной итерации, в итоге получается массив y по заданному массиву х и начальному значению y_start

При этом во всех методах не нужно будет писать множество аргументов, только что-то типа y=solver.Solve(x, y_start), внутри он вызывает y_next=this.MakeStep(x,y_previous), а MakeStep внутри вызывает f=this.f(x,y).
Чот не очень очевидно, что добавить третий параметр с набором коэффициентов так сложно, что нужно использовать ООП вместо f=f(x,y,coeff_array). Которые тоже можно вложить в другие функции.
Хотя может это тоже профдеформация матлабом.
Если этих коэффициентов очень много, и они разнородные по физическому смыслу (я делал расчет нелинейной электрической цепи с эмуляцией насыщения магнитопровода), то совать их в один массив — смерти подобно.
будет как раз кошмар вида coeff=[ад на 10 строчек через запятую].
Нелинейные дифура внутри требуют вычисления подфункций, в них тоже передаются аргументы, так что перепаковывать вот этот массив coeff для скармливания нижестоящему коду будет очень болезненно.
Чем ад на 10 строчек через запятую и многоточие отличается от такого же ада через точку с запятой, когда отдельные имена коэффициентам присвоены?
если я правильно понимаю вопрос, вы спрашиваете, чем лучше
solver.coeff1=k1;
solver.coeff2=k2;
чем запихать в массив coeff=[k1,k2...kn] и скормить функции этот coeff, и дальше его сверху вниз передавать f1(f2(f3(...?
1) имена коэффициентам присвоены. Это удобно, если они разные по физическому смыслу.
2) Внутри функций можно обратиться к коэффициентам по имени.
3) Если захочется добавить новые коэффициенты, то все функции, которые с ними (новыми коэффициентами) не работают, просто остаются, какими были. Дописать коэффициенты «в хвост» — представьте, что они у вас физически разные по смыслу и перемешаются в ходе «апдейтов с дописыванием к концу массива».

2) Ад через точку с запятой в самом начале будет по смыслу совершенно понятными, 1 раз написал и все.

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

Заголовок спойлера
'Вот кусок класса (делал на VBA в экселе)
'для расчета нелинейной электрической цепи
Public R_load, L_load, R2, L2 As Double 'Это сопротивление и индуктивность
Public Im, Iexp, omega, phi, Tp As Double 'это параметры источника тока
Public FLmax, k As Double 'это параметры, отвечающие за нелинейную характеристику
Public w1, w2 As Double 'это коэффициент трансформации (моделировался трансформатор)
Private this_curveCoeffs As CurveCoeffs 'это коэффициенты в относительных единицах, отвечающие за нелинейную характеристику



Наверное это эквивалентно передаче не массива коэффициентов, а структуры с коэффициентами. Но емнип если такое попытаться скомпилировать в mex, оно будет ругаться и требовать assert с жёстко прописанными размерами и типами входных параметров.

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

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

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

mex-файлы — для симулинка, имеется в виду?
Я с симулинком раньше возился-возился, потом проклял его тормознутость, и стал моделировать почти все самописным кодом.

Нет, я mex для матлаба делал, чтоб шустрее считал.
Я симулинком почти не пользовался, но наверное это аналог Accelerate симуляции.

Я бы дальше пошёл здесь.


Сделал бы класс Solver, в котором есть член rhp, который отвечает за правую часть. У rhp есть метод call(x, y), который вызывает собственно функцию, и сколько-то полей, отвечающих за параметры. Примерно так:


classdef Circuit
    properties
        ...
    end
    methods
        function dydx = call(self, x, y)
            ...
        end
    end
end

classdef ODESolver
    properties
        rhp
        y_start
        y_prev
        y_next
        x_start
        x_prev
        x_next
    end
    methods
        function solver = ODESolver(rhp, x_start, y_start)
            solver.rhp = rhp
            solver.x_start = x_start
            solver.y_start = y_start
            solver.x_prev = x_start
            solver.y_prev = y_start
        end
        function solution = solve(self, nsteps, dx)
            ...
        end
        ...
    end
end

Использование в стиле


circuit = Circuit
circuit.coeff1 = coeff1
...

solver = ODESolver(circuit, 0.0, y_start)
solution = solver.solve(1000, 0.01)

Профит в том, что ODESolver больше не отвечает за вычисление самой правой части, это забота rhp внутри него.

UFO just landed and posted this here
В этом контексте объект А, который вызывает метод у B, не рассматривает его как набор данных, который надо передать в нужную процедуру, а считает его «умной коробочкой»
Ах если бы так всегда люди делали — был бы рай…
Люди разные: некоторые мыслят глаголами(глагол — это действие), некоторые — существительными (предметами).
Лично меняя ООП очаровало именно философией, когда программа есть совокупность объектов, обменивающихся сообщениями (т.е. генерирующих события).
А наследование, полиморфизм и прочую атрибутику ООП я уже потом освоил.
Всё что должно быть более одного лучше превратить в объекты. Казалось не логичным почему в Java или C# любой простейший код обязательно должен быть обрамлен в объект — но ведь может быть несколько экземпляров запущенной программы.
И конечно полиморфизм, иначе это просто структуризация, а далее «визитор», множественная диспетчеризация.
Что у автора за отношение к пробелам, что он боится обрамлять в них даже операторы сравнения? Удивляюсь что он в тексте их ставить не забывает.

Всё думал, что ближе к концу будет какое-то откровение… Ан нет.

Sign up to leave a comment.

Articles