среда, 8 сентября 2010 г.

Доводы против with

Это перевод The with-statement considered harmful. Автор: Hallvard Vassbotn.

(Заголовок обыгрывает знаменитую статью Эдсгера Дейкстры «Go To Statement Considered Harmful», которую на русском обычно называют «Доводы против оператора GOTO»)

Как уже упоминалось многими людьми в различных местах (newsgroups, блоги, QC и т.д.), конструкция with в Delphi может быть опасна для читабельности, управляемости и надёжности кода. Не говоря уже про то, что она делает сложнее отладку ;)

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

Рассмотрим такой код:
unit Foo; 
type 
  TGadget = class 
    bar: integer; 
  end; 
//----
unit Bar; 
var 
  a: TGadget; 
  foo: integer 
begin 
  with a do 
    foo := 123; 
end.
Это версия 1. Код успешно компилируется и работает корректно. Теперь поставщик компонента на другой стороне земного шара делает обновление компонента (класса TGadget):
unit Foo; 
type 
  TGadget = class 
    bar: integer; 
    foo: integer; // Новая возможность! 
  end;
Затем Вася перекомпилирует свой код с новой версией компонента (а если вы не используете/обновляете компоненты - это может быть код вашего коллеги):
unit Bar; 
var 
  a: TGadget; 
  foo: integer 
begin 
  with a do 
    foo := 123; 
end.
Сейчас, в D7, этот код скомпилируется без малейшего замечания, но будет вести себя странно в run-time. В будущих версиях компилятора в этом коде мог бы быть warning типа:
Warning: Potential name conflict: foo declared in unit Bar and Foo.TGadget.
В этом случае, чтобы избавиться от warning-га, программист изменил бы код на:
Bar.foo := 123;
или (ещё лучше) он может просто убрать конструкцию with.

IMO, если бы у нас было такое предупреждение для with, это было бы первым шагом в исправлении одного из очень немногих кривых мест в остальном элегантного языка Object Pascal. (и до того, как вы спросите, да, это есть в QC).

24 комментария:

  1. Когда-то я тоже довольно часто использовал with. И для меня выглядели странными высказывания некоторых людей, которые говорили об опасности использования with и о том, что они теперь его используют очень редко или не используют вообще (кажется, это был DRON на DK). Я тоже думал: "ну, с мной же такого не может произойти, ведь я внимательно слежу за своим кодом!". Поэтому я выкинул это из головы и продолжил жить дальше.

    Пока, в один прекрасный день программа не стала себя странно вести. Нет, она не вылетала, не выкидывала исключение - она просто вела себя "как-то не так". Я потратил чуть ли не день, закопавшись в отладчик, пытаясь понять как же это "не так". В итоге я выяснил, что нужный метод не вызывается вовсе - бряк, поставленный на него не срабатывает. Мои глаза чуть не вылезли, когда я увидел, что код вызова этого метода вообще-то выполняется. Да, про то обсуждение with я уже забыл, и даже не обратил внимание, что он здесь используется - блок кода был большим. Поэтому я закопался в ассемблерный отладчик только для того, чтобы увидеть, что вызывается метод не того объекта. После чего я увидел with и долго стучал головой о стол.

    Когда такое произошло во второй раз (окей, в этот раз я потратил меньше времени), я вспомнил о том обсуждении with на DK и сказал себе: всё, стоп, хватит с меня этого. Не пойти ли этому with в баню. С того момента я не использую with. Каждый раз, когда у меня длинная переменная или цепочка (кандидаты для использования with) - я завожу переменную-псевдоним. Всё, точка. Теперь у меня нет и не может быть таких проблем. Кроме того, в новых Delphi завести псевдоним даже проще, чем написать with - благо есть рефакторинг "Introduce variable".

    Ладно, может быть такие временные отказы и не слишком серьёзная причина, но, как я уже сказал, настоящая проблема не столько в них, сколько в том, что вы не можете это контролировать. Вы можете изменить какой-то класс и даже не заметить как поплыла ваша программа. Вы сдаёте программу заказчику, а через два месяца он вам звонит и говорит: "а какого чёрта не работает функционал X, если мы его проверяли в январе?". И когда вы начнёте копаться в этом, вы обнаружите, что with стал вызывать другой метод, из-за чего нарушилась вся логика уже отлаженного кода, который вы полгода уже не трогали.

    ОтветитьУдалить
  2. Ну хз, для фунок вида:
    procedure SetDefaultParams(AMyType: TMyType);
    begin
    with AMyType do
    begin
    Param1 := 0;
    Param2 := 3;
    end;
    end;
    Он полезен.

    ОтветитьУдалить
  3. После драки кулаками не машут, но согласен с Adrian Dankiv.
    И теперь вместо
    with TMyVeryImportantClassForExample.Create(nil) do
    try
    FirstProperty := SomeValue1;
    SecondProperty := SomeValue2;
    ThirdProperty := SomeValue3;
    Result := Execute;
    finally Free;
    end;
    надо будет писать
    var
    MyVeryImportantClassForExample: TMyVeryImportantClassForExample;
    ...
    MyVeryImportantClassForExample := TMyVeryImportantClassForExample.Create;
    try
    MyVeryImportantClassForExample.FirstProperty := SomeValue1;
    MyVeryImportantClassForExample.SecondProperty := SomeValue2;
    MyVeryImportantClassForExample.ThirdProperty := SomeValue3;
    Result := MyVeryImportantClassForExample.Execute;
    finally FreeAnNil(MyVeryImportantClassForExample)
    end;

    По-моему, будет хуже. Кроме того, что упрощалось чтение кода, With давал возможность делать объекты "на лету", т.е. сокращалось "писание". Это безотносительно к новой модели подсчёта ссылок.

    По-моему, следовало бы ввести This для передачи экземпляра With в вызываемые внутри процедуры.

    with TMyQuery.Create(nil) do
    try
    Connection := dmCommon.MainConnection;
    SQL.Text := Format('select id,name from %s',[ADictonaryName]);
    Open;
    DoLoadPickList(DictonaryValue.List, This);
    finally Free;
    end;

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

    По-моему With нуждался в модификации, а не в секвестировании. Я говорил среди прочего с Интерсимоном на эту тему, когда он был в Москве. Но такие разговоры мало к чему приводят.

    Мне нравится Using в С#. With мог бы стать таким же интересным. Но, увы.

    ОтветитьУдалить
  4. with позволяет через запятую перечислить несколько переменных/объектов, поэтому так просто ввести This не получится. Хотя в подавляющем большинстве люди с with используют не более одного объекта.
    Мне же понравилась идея (не моя), когда в операторе with для объекта указывается некий алиас, по которому можно обратиться к объекту (по аналогии с обработкой исключений), к примеру:
    with a: TMySuperPuperClass.Create do
    try
    a.Something;
    a.Somethingelse;
    finally
    a.Free;
    end;

    или так (для нескольких объектов):
    with a: Sender, b: Self do
    begin
    a.Caption := b.Caption;
    a.Hint := b.Hint;
    end;

    ОтветитьУдалить
  5. >>> MyVeryImportantClassForExample

    [fun mode on]
    А ещё можно было назвать ThisIsAVeryLongIdentJustToIllustrateMyRidiculousPoint и утверждать, что читаемость ухудшается.
    [fun mode off]

    >>> Мне же понравилась идея (не моя)

    Мне тоже это нравится.

    Есть ещё вариант поскромнее:

    with A do
    begin
    .Caption := 'F'; // обращение к A.Caption
    Caption := 'F'; // обращение к Form.Caption
    end;

    Но увы: в его современном виде with "вреден", и хотя его можно существенно улучшить (даже несколькими способами) - навряд ли его будут менять.

    ОтветитьУдалить
  6. >>ThisIsAVeryLongIdentJustToIllustrateMyRidiculousPoint
    Ничего смешного. Когда для нахождения точки действия приходится продираться глазами через длинную спецификацию, читабельность кода естественно снижается.
    Чтобы не выдумывать, реальный пример из Embarcadero:
    MainImage.Picture.Bitmap.Canvas.Brush.Color := MainPanel.Color;
    MainImage.Picture.Bitmap.Canvas.FillRect(Rect(0,0,FImages.Width, FImages.Height));
    Я считаю, что With здесь однозначно улучшил бы читабельность кода.

    ОтветитьУдалить
  7. По-моему, вы меня не поняли.

    var
    A: TCanvas;
    begin
    A := MainImage.Picture.Bitmap.Canvas;
    A.Brush.Color := MainPanel.Color;
    A.FillRect(Rect(0,0,FImages.Width, FImages.Height));
    end;

    ОтветитьУдалить

  8. >>По-моему, вы меня не поняли.
    >>
    >>var
    >>A: TCanvas;
    >>begin
    >>A := MainImage.Picture.Bitmap.Canvas;

    О, я вас понял. Использовать переменную для хранения ссылки - вполне разумно. Но так ли удобно? Я вижу, что программисты сплошь и рядом предпочитают "копипастить" спецификацию, чем заводить лишний идентификатор, которому надо дать имя, которое должно быть в духе остальных идентификаторов, которые все часто довольно далеко от текущего контекста.
    Да, напрашивается рефакторинг кода введением переменной. Но простое "а" в отрыве от контекста - странный идентификатор, нуждающийся в комментариях, которые никто не любит писать. Можно говорить о сознательности и квалификации, но такова уж природа человеческая.
    Человек пишущий и человек читающий, даже если это один и тот же организм, - люди разные. И как раз With помогает свести интересы этих людей - одному не надо уходить из контекста для сокращения спецификации, а другому не надо каждую строчку начинать с "отче наш".
    With - полезный оператор, а не вредный. Если вы считаете иначе, то не используйте его. С чего это вдруг кто-то решил, что для меня что-то вредно? Это просто прикрытие недостаточной квалификации разработчиков языка, которые не имели готовой конструкции в LLVM и не смогли сделать соответствующую трансляцию из Паскаля. Уж сколько раз твердили свету, что GoTo - воплощение вселенского зла. И что? И ничего, GoTo - живее всех живых. Так что лукавые разглагольствования о "вредности" - лишь жалкие потуги прикрыть голое место. И расчёт на то, что люди должны в это верить, меня лично сильно раздражает.
    Я надеюсь, что в Embarcadero найдётся реальный паскалист, который вернёт With в язык. И конечно, идея с псевдонимами - очень хороша. Она прекрасно согласуется с текущими конструкциями - try..except. И вполне оставляет место для работы и без идентификатора. Но найдётся ли такой человек? Услышит ли он меня?
    Я написал уже письмо с этими соображениями в Embarcadero. Может, ещё кто-нибудь поддержит With?

    ОтветитьУдалить
    Ответы
    1. Полностью согласен! Не понимать этого и призывать отказаться от "with" может только фрик или враг Паскаля!

      Удалить
  9. Анонимный8 июня 2013 г., 2:17

    Этот код не эквивалентен.
    Возможен вариант, когда MainImage.Picture.Bitmap.Canvas.Brush.Color := MainPanel.Color; сбрасывает внутренний объект для Canvas (удаляет и создает новый), т.е. при следующем вызове MainImage.Picture.Bitmap.Canvas уже другой.
    То же замечание относится к "with", поскольку он неявно работает по второй схеме (через переменную)

    ОтветитьУдалить
  10. Дата поста
    >> СРЕДА, 8 СЕНТЯБРЯ 2010 Г.

    Дата тикета
    Date Reported: 10/16/2002 3:00:35 AM

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

    ОтветитьУдалить
  11. Не уловил к чему это.

    1. Peter Morris добавил тикет на QC в октябре 2002.
    2. Hallvard Vassbotn написал "The with-statement considered harmful" в августе 2004.
    3. Александр Алексеев сделал перевод "The with-statement considered harmful" в сентябре 2010.

    Вы к тому, что 12 лет не могут сделать warning? Значит разработчики Delphi считают целесообразнее направить усилия на другие тикеты.

    ОтветитьУдалить
  12. Какая чушь!

    Вам не приходило в голову, что ВСЕ программисты на Delphi неявно используют оператор "with"?
    И даже те, кто пишет "...Когда-то я тоже довольно часто использовал with... После чего я увидел with и долго стучал головой о стол..."

    Почему чушь? Да просто потому, что раскрытие методов класса всегда идет с большим обрамляющим неявным with Self do...
    Вот простейший пример "Hellow Word":

    var
    Form1: TForm1;

    implementation

    {$R *.dfm}

    procedure TForm1.Button1Click(Sender: TObject);
    begin
    Caption := 'Hellow, World!'
    end;

    что абсолютно эквивалентно:

    procedure TForm1.Button1Click(Sender: TObject);
    begin
    with Self do
    Caption := 'Hellow, World!'
    end;

    Понятно, что это касается любых классов, не только визуальных...
    Не понимать этого и призывать отказаться от "with" может только фрик или враг Паскаля!

    Поэтому пример того как могло бы быть в случае с точкой - вообще за пределом разумного:

    with A do
    begin
    .Caption := 'F'; // обращение к A.Caption
    Caption := 'F'; // обращение к Form.Caption
    end;

    потому что:
    Caption := 'F'; // обращение к Form.Caption

    это на самом деле:
    Self.Caption := 'F'; // обращение к Form.Caption

    и потому (при гипотетическом внедрении Вашего глупого предложения) всё равно потребовало бы оставить там точку:

    with A, Self do
    begin
    .Caption := 'F'; // обращение к A.Caption
    .Caption := 'F'; // обращение к Form.Caption // - здесь ментальная ошибка абсолютно глупого предложения ибо точка ТОЖЕ нужна
    end;

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

    PS: И вообще, посмотрел я Ваши записи по поводу Delphi - они мне напоминают "Вредные советы" Остера...

    Введу-ка я у себя в конторе ещё один фильтр при приеме на работу:
    раньше - стоит в резюме С/С++ - в корзину, оканчиваешь вызов функции/процедуры без параметров пустыми скобками? ... и т.д. и т.п. - в корзину,
    теперь добавлю: читал ли опусы ГанСмокера? Как относишься, - со многим не согласен, но в целом одобряешь? - В корзину!

    ОтветитьУдалить
    Ответы
    1. И, кстати, этот Ваш гипотетический пример с "фальшивой" точкой

      with A do
      begin
      .Caption := 'F'; // обращение к A.Caption
      Caption := 'F'; // обращение к Form.Caption
      end;

      прекрасно работает в уже действуюшей Delphi без всяких нововведений (и без этих пресловутых точек), достаточно только begin/end убрать!

      Вот, смотрите:

      with A do
      Caption := 'F'; // обращение к A.Caption
      Caption := 'F'; // обращение к Form.Caption

      :):):):):):)

      Удалить
    2. > враг Паскаля

      Удачи в отладке:

      var
      R: TRect;
      begin
      with R do
      Left := Width div 2;
      ....

      Удалить
    3. Удачи в КАКОЙ отладке, GunSmoker?

      Здесь всё идеально! Именно так, как и задумано создателем Паскаля:

      procedure TForm1.Button1Click(Sender: TObject);
      var
      R : TRect;
      begin
      R.Left := 0; // для строгости - инициализация

      // Поехали:

      Memo1.Lines.Add(Format('%d'^i'%d',[R.Left,Width])); // Контроль

      with R do
      Left := Width div 2;

      // что эквивалентно:
      //
      // with Self, R do
      // Left := Width div 2;
      //
      // или в развернутом виде:
      //
      // R.Left := Self.Width div 2;
      //
      // или в окончательном виде:
      //
      // R.Left := Form1.Width div 2;

      Memo1.Lines.Add(Format('%d'^i'%d',[R.Left,Width])); // Контроль
      end;

      Вам лучше сменить профессию, GunSmoker, ну или уйти из Паскаля/Delhi, а то и вообще - из программирования!
      Не Ваше это...

      И уж точно - не писать свои советы.
      Или сопровождать их припиской - "Вредные Советы Остера-от-Паскаля GunSmokera"!

      PS: Лишний раз убедился, что ввёл абсолютно правильный критерий при приёме у себя на работу: Одобряешь GunSmokera - резюме в корзину!

      Удалить
    4. Если б ты нормально общался, я бы даже разжевал и в рот положил. А так - на, изучай. Может дойдёт, наконец.

      Удалить
    5. Это ТЫ иди изучай:

      procedure TForm1.Button1Click(Sender: TObject);
      var
      R : TRect;

      procedure ShowAll(const Comment : string);
      begin
      with Memo1.Lines,R do
      Add(Format(DupeString('%.3d'^i,4)+'%s',[Left,Right,Width,Self.Width,Comment]))
      end;

      begin
      with Memo1,Lines,Font do begin
      Name := 'Courier New';
      Style := Style + [fsBold,fsItalic];
      Add('R.Left'^i'R.Right'^i'R.Width'^i'Width'^i'Comment')
      end;

      R := TRect.Create(Point(100,300),Point(500,700)); ShowAll('Initialization'^m^j);

      with R do Left := Width div 2; ShowAll('R.Left := R.Width div 2');
      with R do Left := Self.Width div 2; ShowAll('R.Left := Form1.Width div 2'^m^j);
      end;

      и разжевывать я тебе не буду... Может до тебя самого дойдёт, наконец.



      Удалить
    6. Анонимный21 мая 2016 г., 10:06

      Теперь понял, что при трудоустройстве буду говорить, что одобряю статьи GunSmoker'a. И если выяснится, что я устраиваюсь к великому meligo на работу - то сам выброшу своё резюме в корзину.

      Удалить
    7. искал другое, случайно наткнулся, внесу свою точку зрения в спор, хоть и запоздало.

      meligo,
      это вы несете полную чушь. Причем здесь неявное и явное обращение к объекту через self? Я кажется читал где-то эту аналогию, но она приведена лишь для упрощения понимания, и вы неверно интерпретировали ее. А вы вообще отлаживали программу хоть раз? Вижу, что толком нет, раз такой бред несете. Вы даже разницу не понимаете между контекстом имеющим 1 и n-уровней. Обращение к полям, св-вам и методам внутри объекта и with - совершенно разные вещи. Первое как раз вполне логично и очевидно. Хотя и здесь не помешала бы возможность однозначной короткой ссылки вроде "." или еще как-нибудь, но в целом и "self." неплохо. Просто было бы быстрее, нагляднее и удобнее. Мне лично каждый раз self в лом писать. И избавила бы от контроля состава тек. объекта (напр. удаление метода тек. объекта, но в др. модуле имеется с таким же именем и аргументами, либо поле и var-переменная).
      По вашей недалекой логике можно приравнять все конструкции, которые дают одинаковый результат. Вам не приходило в голову, если "вы так считаете", то это еще совсем не аргумент?

      >> прекрасно работает в уже действуюшей Delphi без всяких нововведений (и без этих пресловутых точек), достаточно только begin/end убрать
      Вы думаете, что это смешно? Ну только если вам самому нравится выглядеть идиотом.

      Сразу видно, что meligo просто никогда не имел дело с большими проектами. У нас на работе тоже били такие, которые петрушку обсуждали с видом академиков, а как дело доходило до реального проекта, то, простите, какали себе в штаны...
      и кстати, meligo, ваш "Hellow world" уже говорит о многом... Так что удачи вам и дальше мести улицы, "работодатель"..
      И научитесь деликатности в общении, вместо того, чтобы брызгать помойной слюной, восполняя свою недооцененность.

      Вообще говоря, по новым возможностям среды очень не хватает литературы.
      А GunSmoker молодец, что ведет такой блог. Я его полностью поддерживаю. И многие вещи очень полезные пишет. Да, по некоторым вопросам есть свое мнение. Но для того и существуют комментарии - для дискуссии и прояснения спорных моментов. Единственное, не мешало бы комментарии сделать поудобнее.

      От себя добавлю, что сам недолюбливал никогда with с института. Поскольку не люблю неоднозначность. А она вытекала даже на тех учебных примерах, которые я любил "навернуть" по полной. Конструкция полезная и в большинстве случаев проблем с ней не возникает, но доработать бы ее не мешало.

      На мой взгляд был бы идеален вариант, например:
      With
      LongChainOfObject1 => o1, //так конечно наглядней
      LongChainOfObject2 eq o2 //либо так, не суть важно
      do begin
      o1.field1 := 111;
      o2.foeld1 := 222;
      end;

      Улучшение налицо. Используем удобное для себя сокращение без доп. инициализации переменных, и абсолютная однозначность без нарушения гармонии языка, и что немаловажно - наконец-то нормальная отладка внутри конструкции. Плюс, также еще повторюсь, для текущего объекта "." по мне была бы вообще отличным вариантом (соображения выше).

      Удалить
  13. я... даже не знаю кого одобрить. Но meligo конкретно жжет...

    ОтветитьУдалить
    Ответы
    1. Да ничего он не жжет, а только корчит из себя непонятно кого. Видно, что паскаль он учил когда-то. Но в ООП и делфи у него, судя по всему, знания начального уровня.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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