среда, 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).

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

  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 на работу - то сам выброшу своё резюме в корзину.

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

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

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

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

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

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

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