Программирование методов с использованием строк

Цель лабораторной работы : изучить методы в языке C#, правила работы с символьными данными и с компонентом ListBox. Написать программу для работы со строками.

Методы

Метод – это элемент класса, который содержит программный код. Метод имеет следующую структуру:

[атрибуты] [спецификторы] тип имя ([параметры])

Тело метода;

Атрибуты – это особые указания компилятору на свойства метода. Атрибуты используются редко.

Спецификаторы – это ключевые слова, предназначенные для разных целей, например:

· Определяющие доступность метода для других классов:

o private – метод будет доступен только внутри этого класса

o protected – метод будет доступен также дочерним классам

o public – метод будет доступен любому другому классу, который может получить доступ к данному классу

· Указывающие доступность метода без создания класса

· Задающие тип

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

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

Параметры – это список переменных, которые можно передавать в метод при вызове. Каждый параметр состоит из типа и названия переменной. Параметры разделяются запятой.

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

Пример метода, вычисляющего выражение:

public double Calc(double a, double b, double c)

return Math.Sin(a) * Math.Cos(b);

double k = Math.Tan(a * b);

return k * Math.Exp(c / k);

Перегрузка методов

Язык C# позволяет создавать несколько методов с одинаковыми именами, но разными параметрами. Компилятор автоматически подберёт наиболее подходящий метод при построении программы. Например, можно написать два отдельных метода возведения числа в степень: для целых чисел будет применяться один алгоритм, а для вещественных – другой:

///

/// Вычисление X в степени Y для целых чисел

///

private int Pow(int X, int Y)

///

/// Вычисление X в степени Y для вещественных чисел

///

private double Pow(double X, double Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

else if (Y == 0)

Вызывается такой код одинаково, разница лишь в параметрах – в первом случае компилятор вызовет метод Pow с целочисленными параметрами, а во втором – с вещественными:

Параметры по умолчанию

Язык C# начиная с версии 4.0 (Visual Studio 2010) позволяет задавать некоторым параметрам значения по умолчанию – так, чтобы при вызове метода можно было опускать часть параметров. Для этого при реализации метода нужным параметрам следует присвоить значение прямо в списке параметров:

private void GetData(int Number, int Optional= 5 )

Console.WriteLine("Number: {0}", Number);

Console.WriteLine("Optional: {0}", Optional);

В этом случае вызывать метод можно следующим образом:

GetData(10, 20);

В первом случае параметр Optional будет равен 20, так как он явно задан, а во втором будет равен 5, т.к. явно он не задан и компилятор берёт значение по умолчанию.

Параметры по умолчанию можно ставить только в правой части списка параметров, например, такая сигнатура метода компилятором принята не будет:

private void GetData(int Optional= 5 , int Number)

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

private void Calc(int Number)

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

Console.WriteLine(n);

На экране появится число 1, то есть, не смотря на изменение переменной в методе Calc, значение переменной в главной программе не изменилось. Это связано с тем, что при вызове метода создаётся копия переданной переменной, именно её изменяет метод. При завершении метода значение копий теряется. Такой способ передачи параметра называется передачей по значению .

Чтобы метод мог изменять переданную ему переменную, её следует передавать с ключевым словом ref – оно должно быть как в сигнатуре метода, так и при вызове:

private void Calc(ref int Number)

Console.WriteLine(n);

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

Если метод использует переменные по ссылке только для возврата значений и ему не важно что в них было изначально, то можно не инициализировать такие переменные, а передавать их с ключевым словом out. Компилятор понимает, что начальное значение переменной не важно и не ругается на отсутствие инициализации:

private void Calc(out int Number)

int n; // Ничего не присваиваем!

Тип данных string

Для хранения строк в языке C# используется тип string. Для того, чтобы объявить (и, как правило, сразу инициализировать) строковую переменную, можно написать следующий код:

string a = "Текст";

string b = "строки";

Над строками можно выполнять операцию сложения – в этом случае текст одной строки будет добавлен к тексту другой:

string c = a + " " + b; // Результат: Текст строки

Тип string на самом деле является псевдонимом для класса String, с помощью которого над строками можно выполнять ряд более сложных операций. Например, метод IndexOf может осуществлять поиск подстроки в строке, а метод Substring возвращает часть строки указанной длины, начиная с указанной позиции:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Результат: 14 (счёт с 0)

string b = a.Substring(3, 5); // Результат: DEFGH

Если требуется добавить в строку специальные символы, это можно сделать с помощью escape-последовательностей, начинающихся с обратного слэша:

Компонент ListBox

Компонент ListBox представляет собой список, элементы которого выбираются при помощи клавиатуры или мыши. Список элементов задается свойством Items . Items – это элемент, который имеет свои свойства и свои методы. Методы Add , RemoveAt и Insert используются для добавления, удаления и вставки элементов.

Объект Items хранит объекты, находящиеся в списке. Объект может быть любым классом – данные класса преобразуются для отображения в строковое представление методом ToString. В нашем случае в качестве объекта будут выступать строки. Однако, поскольку объект Items хранит объекты, приведённые к типу object, перед использованием необходимо привести их обратно к изначальному типу, в нашем случае string:

string a = (string)listBox1.Items;

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

Когда я начинал программировать на C++ и усиленно штудировал книги и статьи, то неизменно натыкался на один и тот же совет: если нам нужно передать в функцию какой-то объект, который не должен изменяться в функции, то он всегда должен передаваться по ссылке на константу (ППСК), за исключением тех случаев, когда нам нужно передать либо примитивный тип, либо сходную с оными по размеру структуру. Т.к. за более чем 10 лет программирования на C++ я очень часто встречался с этим советом (да и сам его давал неоднократно), он давно «впитался» в меня - я на автомате передаю все аргументы по ссылке на константу. Но время идёт и уже прошло 7 лет, как мы имеем в своём распоряжении C++11 с его семантикой перемещения, в связи с которой я всё больше и больше слышу голосов, подвергающих старую добрую догму сомнениям. Многие начинают утверждать, что передача по ссылке на константу это прошлый век и теперь нужно передавать по значению (ППЗ). Что стоит за этими разговорами, а также какие выводы мы можем из этого всего сделать, я и хочу обсудить в этой статье.

Книжная мудрость

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

Мэйерс

Хорошо, вот мы имеем класс, в котором все параметры передаются по ссылке, есть ли с этим классом какие-то проблемы? К сожалению, есть, и эта проблема лежит на поверхности. У нас в классе функционально 2 сущности: первая принимает значение на этапе создания объекта, а вторая позволяет изменить ранее установленное значение. Сущности-то у нас две, а вот функции четыре. А теперь представьте, что у нас может быть не 2 подобных сущности, а 3, 5, 6, что тогда? Тогда нас ждёт сильное раздувание кода. Поэтому, чтобы не плодить массы функций, появилось предложение отказаться от ссылок в параметрах вообще:

Template class Holder { public: explicit Holder(T value): m_Value{move(value)} { } void setValue(T value) { m_Value = move(value); } const T& value() const noexcept { return m_Value; } private: T m_Value; };

Первое преимущество, которое сразу бросается в глаза, заключается в том, что кода стало значительно меньше. Его даже меньше чем в самом первом варианте, за счёт удаления const и & (правда, добавили move ). Но ведь нас всегда учили, что передача по ссылке производительнее, чем передача по значению! Так оно было до C++11, так оно и есть до сих пор, но теперь, если мы посмотрим на этот код, то увидим, что копирования здесь не больше чем в первом варианте, при условии, что у T есть конструктор перемещения . Т.е. сама по себе ППСК была и будет быстрее ППЗ, но ведь код как-то использует переданную ссылку, и зачастую этот аргумент копируется.

Однако, это не вся история. В отличии от первого варианта, где у нас есть только копирование, тут добавляется ещё и перемещение. Но ведь перемещение это дешёвая операция, правда? На эту тему, у рассматриваемой нами книги Мэйерса, тоже есть глава («Item 29»), которая озаглавлена так: «Assume that move operations are not present, not cheap and not used». Основная мысль должна быть ясна из названия, но если хочется подробностей, то всенепременно ознакомьтесь - я на этом останавливаться не буду.

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

Видит он его в том, что в случае передачи rvalue, т.е. какого-то такого вызова: Holder holder{string{"me"}}; , вариант с ППСК даст нам копирование, а вариант с ППЗ даст нам перемещение. С другой стороны, если передача будет такой: Holder holder{someLvalue}; , то ППЗ однозначно проигрывает за счёт того, что он выполнит и копирование, и перемещение, тогда как в варианте с ППСК будет только одно копирование. Т.е. получается, что ППЗ, если рассматривать сугубо эффективность, это некоторый компромисс между количеством кода и «полноценной» (через && ) поддержкой семантики перемещения.

Именно поэтому Скотт так тщательно сформулировал свой совет и так осторожно его продвигает. Мне даже показалось, что он приводит его нехотя, как бы под давлением: он не мог не разместить рассуждения на эту тему в книге, т.к. она довольно широко обсуждалась, а Скотт всегда был сборщиком коллективного опыта. Кроме того, уж очень мало доводов он приводит в защиту ППЗ, а вот тех, что ставят эту «технику» под сомнение, он приводит немало. Мы ещё рассмотрим его доводы «против» в последующих разделах, здесь же мы кратко повторим аргумент, который Скотт приводит в защиту ППЗ (мысленно добавляем «если объект поддерживает перемещение и оно дёшево» ): позволяет избежать копирования при передаче rvalue-выражения в качестве аргумента функции. Но хватит мучить книгу Мэйерса, давайте уже перейдём к другой книге.

Кстати, если кто-то читал книгу и удивляется, что я не привожу здесь вариант с тем, что Мэйерс называл универсальными ссылками (universal references) - теперь они известны как пробрасывающие ссылки (forwarding references), - то это легко объясняется. Я рассматриваю только ППЗ и ППСК, т.к. вводить шаблонные функции для методов, которые шаблонами не являются, только ради того, чтобы поддержать передачу по ссылкам обоих типов (rvalue/lvalue) считаю дурным тоном. Не говоря уже о том, что код получается другим (больше нет константности) и несёт с собой другие проблемы.

Джосаттис и компания

Последней книгой мы рассмотрим «C++ Templates» , она же является наиболее свежей из всех упомянутых в этой статье книг. Вышла она под конец 2017 года (а внутри книги вообще 2018 указан). В отличии от других книг, эта целиком посвящена шаблонам, а не советам (как у Мэйерса) или C++ в целом, как у Страуструпа. Поэтому и плюсы/минусы тут рассматриваются с точки зрения написания шаблонов.

Данной теме посвящена целая глава 7, которая имеет красноречивое название «By value or by reference?». В этой главе авторы довольно кратко, но ёмко описывают все методы передачи со всеми их плюсами и минусами. Анализ эффективности здесь практичеки не приводится, и как должное принимается то, что ППСК будет быстрее ППЗ. Но при всём при этом в конце главы авторы рекомендуют использовать ППЗ для шаблонных функций по умолчанию. Почему? Потому что используя ссылку, шаблонные параметры выводятся полностью, а без ссылки «разлагаются» (decay), что благоприятно сказывается на обработке массивов и строковых литералов. Авторы считают, что если уж для какого-то типа ППЗ окажется неэффективным, то всегда можно использовать std::ref и std::cref . Такой себе совет, честно говоря, много вы видели желающих использовать вышеозначенные функции?

Что же они советуют касательно ППСК? Они советуют использовать ППСК тогда, когда производительность критична или есть другие весомые причины не использовать ППЗ. Конечно, мы здесь говорим только о шаблонном коде, но этот совет прямо противоречит всему, чему учили программистов на протяжении десятка лет. Это не просто совет рассмотреть ППЗ как альтернативу - нет, это совет альтернативой сделать ППСК.

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

Сетевая мудрость

Т.к. живём в век интернета, то на одну книжную мудрость полагаться не стоит. Тем более, что многие авторы, которые раньше писали книги, теперь просто пишут блоги, а от книг отказались. Одним из таких авторов является Герб Саттер, который в мае 2013 года опубликовал в своём блоге статью «GotW #4 Solution: Class Mechanics» , которая хоть и не является целиком посвящённой освещаемой нами проблеме, всё-таки задевает её.

Итак, в первоначальном варианте статьи Саттер просто повторил старую мудрость: «передавайте параметры по ссылке на константу», но этого варианта статьи мы уже не увидим, т.к. в статье находится обратный совет: «если параметр всё равно будет скопирован, тогда передавайте его по значению». Опять пресловутое «если». Почему Саттер изменил статью, и откуда я об этом узнал? Из комментариев. Почитайте комментарии к его статье, они, кстати, интереснее и полезнее самой статьи. Правда, уже после написания статьи, Саттер всё-таки поменял своё мнение и такого совета он больше не даёт. Изменившееся мнение можно обнаружить в его выступлении на CppCon в 2014 году: «Back to the Basics! Essentials of Modern C++ Style» . Посмотрите обязательно, мы же перейдём к следующей интернет-ссылке.

А на очереди у нас главный программистский ресурс 21 века: StackOverflow. А точнее ответ, с количеством положительных реакций превышающим 1700 на момент написания этой статьи. Вопрос звучит так: What is the copy-and-swap idiom? , и, как должно быть понятно из названия, не совсем по теме, что мы рассматриваем. Но в своём ответе на этой вопрос, автор затрагивает и интересующую нас тему. Он тоже советует использовать ППЗ «если аргумент всё равно будет скопирован» (пора уже и на это аббревиатуру вводить, ей Богу). И в целом этот совет выглядит вполне уместным, в рамках его ответа и обсуждаемого там operator= , но автор берёт на себя смелость давать подобный совет в более широком ключе, а не только в этом частном случае. Более того, он идёт дальше всех рассмотренных нами ранее советов и призывает делать это даже в C++03 коде! Что же подвигло автора на подобные умозаключения?

Судя по всему, основное вдохновение автор ответа черпал из статьи ещё одного книжного автора и по совместительству разработчика Boost.MPL - Дэйва Абрахамса. Статья называется «Want Speed? Pass by Value.» , и была она опубликована ещё в августе 2009 года, т.е. за 2 года до принятия C++11 и введения семантики перемещения. Как и в предыдущих случаях, рекомендую читателю самостоятельно ознакомиться со статьей, я же приведу основные доводы (довод, в сущности, один), которые Дэйв приводит в пользу ППЗ: нужно использовать ППЗ, потому что с ним хорошо работает оптимизация «пропуск копирования» (copy elision), которая отсутствует при ППСК. Если почитать комментарии к статье, то можно увидеть, что продвигаемым им совет не является универсальным, что подтверждает сам автор, отвечая на критику комментаторов. Тем не менее статья содержит явный совет (guideline) использовать ППЗ, если аргумент всё равно будет скопирован. Кстати, кому интересно, можете почитать статью «Want speed? Don’t (always) pass by value.» . Как должно быть ясно из названия, это статья является ответом на статью Дэйва, так что если прочли первую, то и эту прочтите обязательно!

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

Т.к. различные стандарты и рекомендации сейчас тоже размещаются в сети, то я решил отнести этот раздел к «сетевой мудрости». Итак, здесь я хотел бы поговорить о двух источниках, назначение которых - сделать код C++ программистов лучше, путём предоставления последним советов (guidelines) по тому, как этот самый код писать.

Первый набор правил, который я хочу рассмотреть, явился последней каплей, заставившей меня всё-таки взяться за эту статью. Этот набор является частью утилиты clang-tidy и вне её не существует. Как и всё, что связано с clang, эта утилита весьма популярна и уже получила интеграцию с CLion и Resharper C++ (именно так я с ней и столкнулся). Итак, clang-tydy содержит правило modernize-pass-by-value , которое срабатывает на конструкторах, принимающих аргументы посредством ППСК. Это правило предлагает нам заменить ППСК на ППЗ. Более того, на момент написания статьи в описании данного правила содержится ремарка, что это правило пока работает только для конструкторов, но они (кто они?) с удовольствием примут помощь от тех, кто распространит это правило на другие сущности. Там же, в описании, есть и ссылка на статью Дэйва - понятно откуда ноги растут.

Наконец, в завершении рассмотрения чужой мудрости и авторитетных мнений, предлагаю посмотреть на официальные рекомендации по написанию C++ кода: C++ Core Guidelines , основными редакторами которых являются Герб Саттер и Бъярн Страуструп (неплохо, правда?). Так вот, эти рекомендации содержат следующее правило: «For “in” parameters, pass cheaply-copied types by value and others by reference to const» , которое полностью повторяет старую мудрость: ППСК везде и ППЗ для небольших объектов. В описании этого совета приводятся несколько альтернатив, которые предлагается рассмотреть в случае если передача аргументов нуждается в оптимизации . Но в списке альтернатив ППЗ не представлена!

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

Анализ

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

А есть ли преимущество у ППЗ?

Итак, прежде чем рассматривать аргументы «за» и «против», я предлагаю посмотреть какое и в каких случаях преимущество нам даёт передача по значению. Пусть у нас есть какой-то такой класс:

Class CopyMover { public: void setByValuer(Accounter byValuer) { m_ByValuer = std::move(byValuer); } void setByRefer(const Accounter& byRefer) { m_ByRefer = byRefer; } void setByValuerAndNotMover(Accounter byValuerAndNotMover) { m_ByValuerAndNotMover = byValuerAndNotMover; } void setRvaluer(Accounter&& rvaluer) { m_Rvaluer = std::move(rvaluer); } };

Хотя в рамках данной статьи нам интересны только первые д��е функции, я привел четыре варианта, чтобы просто использовать их в качестве контраста.

Класс Accounter - это простой класс, который считает сколько раз он был скопирован/перемещён. А в классе CopyMover у нас реализованы функции, которые позволяют рассмотреть следующие варианты:

    перемещением переданного аргумента.

    Передача по значению, с последующим копированием переданного аргумента.

Теперь, если мы передадим lvalue в каждую из этих функций, к примеру вот так:

Accounter byRefer; Accounter byValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

то получим следующие результаты:

Очевидным победителем является ППСК, т.к. даёт всего одно копирование, тогда как ППЗ даёт одно копирование и одно перемещение.

Теперь попробуем передать rvalue:

CopyMover copyMover; copyMover.setByRefer(Accounter{}); copyMover.setByValuer(Accounter{}); copyMover.setByValuerAndNotMover(Accounter{}); copyMover.setRvaluer(Accounter{});

Получим следующее:

Тут уже однозначного победителя нет, т.к. и у ППЗ, и у ППСК по одной операции, но в силу того, что ППЗ использует перемещение, а ППСК - копирование, можно отдать победу ППЗ.

Но на этом эксперименты наши не заканчиваются, давайте добавим следующие функции для имитации косвенного вызова (с последующей передачей аргумента):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) { copyMover.setByValuer(std::move(byValuer)); } void setByRefer(const Accounter& byRefer, CopyMover& copyMover) { copyMover.setByRefer(byRefer); } ...

Использовать их мы будем точно так же, как делали без них, поэтому повторять код не стану (посмотрите в хранилище, если нужно). Итак, для lvalue результаты будут такими:

Заметьте, что ППСК увеличивает разрыв с ППЗ, оставаясь с единственной копией, тогда как у ППЗ уже целых 3 операции (на одно перемещение больше)!

Теперь передаём rvalue и получаем такие результаты:

Теперь ППЗ имеет 2 перемещения, а ППСК всё то же одно копирование. Можно ли теперь выдвинуть ППЗ в победители? Нет, т.к. если одно перемещение должно быть как минимум не хуже, чем одно копирование, про 2 перемещения мы подобного сказать уже не можем. Поэтому победителя в этом примере не будет.

Мне могут возразить: «Автор, у Вас предвзятое мнение и Вы притягиваете за уши то, что Вам выгодно. Даже 2 перемещения будут дешевле чем копирование!». Я не могу согласиться с подобным утверждением в общем , т.к. то, насколько перемещение быстрее копирования зависит от конкретного класса, но мы ещё рассмотрим «дешёвое» перемещение в отдельном разделе.

Тут мы затронули интересную вещь: мы добавили один косвенный вызов, и ППЗ прибавило в «весе» ровно на одну операцию. Думаю, что не нужно иметь диплом МГТУ для понимания того, что чем больше косвенных вызовов мы имеем, тем больше операций будет выполнено при использовании ППЗ, тогда как для ППСК количество будет оставаться неизменным.

Всё рассмотренное выше вряд ли стало для кого-то откровением, мы могли даже не проводить экспериментов - всё эти числа должны быть очевидны большинству C++ программистов с первого взгляда. Правда, один момент всё же заслуживает пояснения: почему в случае с rvalue у ППЗ нет копирования (или ещё одного перемещения), а есть только одно перемещение.

Что ж, мы рассмотрели разницу в передаче между ППЗ и ППСК, воочию понаблюдав за количеством копий и перемещений. Хотя очевидно, что преимущество ППЗ над ППСК даже в таких простых примерах мягко говоря не очевидно, я всё же, немного кривя душой, сделаю следующий вывод: если мы всё равно будем копировать аргумент функции, то имеет смысл рассмотреть передачу аргумента в функцию по значению. Зачем я сделал этот вывод? Чтобы плавно перейти к следующему разделу.

Если копируем...

Итак, мы добрались до пресловутого «если». Большинство встреченных нами аргументов не призывали повсеместно внедрять ППЗ вместо ППСК, они лишь призывали делать это «если всё равно аргумент будет скопирован». Пришло время разобраться, что не так с этим аргументом.

Хочу начать с небольшого описания того, как я пишу код. В последнее время мой процесс написания кода всё больше походит на TDD, т.е. написание любого метода класса начинается с написания теста, в котором этот метод фигурирует. Соответственно, начиная писать тест, и создавая после написания теста метод, я ещё не знаю буду ли я копировать аргумент. Конечно, не все функции создаются таким образом, часто ещё в процессе написания теста ты совершенно точно знаешь, что там будет за реализация. Но так происходит не всегда!

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

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

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

Void setName(Name name) { m_Name = move(name); }

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

Void setName(Name name) { m_Name = move(name); emit nameChanged(m_Name); }

Есть ли в этом коде проблема? Есть. На каждый вызов setName мы посылаем сигнал, так что сигнал будет послан даже тогда, когда значение m_Name не изменилось. Помимо вопросов производительности, такая ситуация может привести к бесконечному циклу из-за того, что код, который получает вышеозначенное уведомление, каким-то образом приходит к тому, чтобы вызвать setName . Чтобы избежать всех этих проблем подобные методы чаще всего выглядят примерно так:

Void setName(Name name) { if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); }

Мы избавились от вышеописанных проблем, но теперь наше правило «если всё равно копируем...» дало сбой - больше нет безусловного копирования аргумента, теперь мы его копируем только при условии изменения! И что нам теперь делать? Менять интерфейс? Хорошо, давайте изменим интерфейс класса из-за этого исправления. А что если наш класс унаследовал этот метод из некоторого абстрактного интерфейса? Поменяем и там! Не много ли изменений из-за того, что изменилась реализация?

Опять мне могут возразить, мол автор, ты чего тут на спичках экономить вздумал, когда там это условие отработает? Да большую часть вызовов оно будет ложным! А в этом есть уверенность? Откуда? И если я решил экономить на спичках, так разве сам факт того, что мы использовали ППЗ не явился ли следствием именно такой экономии? Я лишь продолжаю «линию партии», ратующую за эффективность.

Конструкторы

Кратко пройдёмся и по конструкторам, тем более что для них есть специальное правило в clang-tidy, которое для других методов/функции пока не работает. Предположим, у нас есть такой класс:

Class JustClass { public: JustClass(const string& justString): m_JustString{justString} { } private: string m_JustString; };

Очевидно, что параметр копируется, и clang-tidy нам сообщит, что было бы неплохо переписать конструктор на такой:

JustClass(string justString): m_JustString{move(justString)} { }

И мне, честно говоря, сложно тут возразить - ведь и правда всегда копируем. И чаще всего, когда мы передаём что-либо через конструктор, мы это что-то копируем. Но чаще не значит всегда. Вот вам другой пример:

Class TimeSpan { public: TimeSpan(DateTime start, DateTime end) { if(start > end) throw InvalidTimeSpan{}; m_Start = move(start); m_End = move(end); } private: DateTime m_Start; DateTime m_End; };

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

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

Что можно вынести из этого раздела? То, что аргумент «если всё равно копируем...» является весьма спорным, т.к. далеко не всегда мы знаем, что копировать будем, а даже когда знаем, мы очень часто не уверены в том, что это так и будет продолжаться в дальнейшем.

Перемещение дёшево

С самого момента появления семантики перемещения, она начала оказывать серьёзное влияние на то, как пишется современный C++-код, и за прошедшее время это влияние только усилилось: оно и немудрено, ведь перемещение так дёшево по сравнению с копированием. Но так ли это? Правда ли, что перемещение это всегда дешёвая операция? Вот с этим мы и попытаемся разобраться в этом разделе.

Большой двоичный объект

Начнём с банального примера, пусть у нас есть такой класс:

Struct Blob { std::array data; };

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

Void Storage::setBlobByRef(const Blob& blob) { m_Blob = blob; } void Storage::setBlobByVal(Blob blob) { m_Blob = move(blob); }

А вызывать эти функции будем так:

Const Blob blob{}; Storage storage; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

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

Прежде чем перейдём к измерениям, давайте попробуем предсказать результат. Итак, у нас есть std::array размером в 4 Кб, который мы хотим сохранить в объекте класса Storage . Как мы выяснили ранее, для ППСК у нас будет одно копирование, тогда как для ППЗ будет одно копирование и одно перемещение. Исходя из того, что array переместить невозможно, для ППЗ будет 2 копирования, против одного для ППСК. Т.е. мы вправе ожидать двукратного превосходства в производительности для ППСК.

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

Этот и все последующие тесты выполнялись на одной машине с использованием MSVS 2017 (15.7.2) и с флагом /O2 .

Практика совпала с предположением - передача по значению получается в 2 раза дороже, потому что для array перемещение полностью эквивалентно копированию.

Строка

Рассмотрим другой пример, обычную строку std::string . Что мы можем ожидать? Мы знаем (я рассматривал это в статье ), что современные реализации различают string двух типов: короткие (в районе 16 символов) и длинные (те, что больше коротких). Для коротких используется внутренний буфер, который представляет собой обычный C-массив из char , а вот длинные уже будут размещаться в куче. Короткие строки нас не интересуют, т.к. результат там будет тот же, что и с БДО, поэтому сосредоточимся на длинных строках.

Итак, имея длинную строку, очевидно, что её перемещение должно быть довольно дёшево (просто переместить указатель), поэтому можно рассчитывать на то, что перемещение строки не должно вообще никак сказаться на результатах, и ППЗ должна дать результат не хуже ППСК. Проверим на практике и получим следующие результаты:

Мы же перейдём к объяснению этого «феномена». Итак, что происходит когда мы копируем существующую строку в уже существующую строку? Давайте рассмотрим банальный пример:

String first{64, "C"}; string second{64, "N"}; //... second = first;

У нас две строки размером в 64 символа, поэтому при их создании внутреннего буфера недостаточно, в результате обе строки размещаются в куче. Теперь мы копируем first в second . Т.к. размеры строк у нас одинаковые, очевидно, что в second выделено достаточно места, чтобы вместить все данные из first , поэтому second = first; будет представлять собой банальный memcpy , не более того. Но если мы рассмотрим слега изменённый пример:

String first{64, "C"}; string second = first;

то здесь уже не будет вызова operator= , но будет вызван конструктор копирования. Т.к. мы имеем дело с конструктором, то существующей памяти в нём нет. Её сначала надо выделить и только потом скопировать first . Т.е. это выделение памяти, а потом memcpy . Как мы с вами знаем, выделение памяти в глобальной куче это, как правило, дорогая операция, поэтому копирование из второго примера будет дороже копирования из первого. Дороже на одно выделение памяти в куче.

Какое это отношение имеет к нашей теме? Самое прямое, ведь первый пример показывает ровно то, что происходит при ППСК, а второй то, что происходит при ППЗ: для ППЗ всегда создаётся новая строка, тогда как для ППСК происходит переиспользование существующей. Разницу во времени выполнения вы уже видели, так что тут добавить нечего.

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

Мне, безусловно, могут возразить, что, мол, string стоит особняком, и большинство типов так не работают. На что я могу ответить следующее: всё, что описано ранее будет справедливо для любого контейнера, который выделяет память в куче сразу под пачку элементов. Кроме того, кто знает какие другие контекстно-зависимые оптимизации применяются в других типах?

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

Сложный тип

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

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

Итак, поехали: Person с 10-ю полями типа string , который мы передаём с помощью ППСК и ППЗ в Storage :

Как вы можете видеть, мы имеем колоссальную разницу в производительности, что после предыдущих разделов не должно было стать для читателей неожиданностью. Также я считаю, что класс Person является достаточно «реальным», чтобы не отметать подобные результаты как абстрактные.

Кстати, когда я готовил эту статью, я подготовил ещё один пример: класс, который использует несколько объектов std::function . По моей задумке он тоже должен был показать преимущество в производительности ППСК над ППЗ, но получилось ровно наоборот! Но я не привожу этот пример здесь не потому, что мне не понравились результаты, а потому, что у меня не нашлось времени разобраться почему же такие результаты получаются. Тем не менее код в хранилище есть (Printers ), тесты - тоже, если у кого-то есть желание разобраться, я был бы рад услышать о результатах исследования. Я же планирую вернуться к этому примеру позже, и если до меня никто этих результатов не опубликует, то я рассмотрю их в отдельной статье.

Итоги

Итак, мы рассмотрели различные плюсы и минусы передачи по значению и по ссылке на константу. Рассмотрели некоторые примеры и посмотрели на производительность обоих методов в этих примерах. Разумеется, эта статья не может и не является исчерпывающей, но, на мой взгляд, в ней достаточно информации, чтобы принять самостоятельное и взвешенное решение по тому, какой же способ лучше использовать. Кто-то может возразить: «зачем использовать один способ, давайте отталкиваться от задачи!». Хотя я согласен с этим тезисом в общем виде, я не согласен с ним в данной ситуации. Я считаю, что в языке может быть только один способ передачи аргументов, который используется по умолчанию .

Что значит по умолчанию? Это значит, что когда я пишу функцию, я не думаю о том, как мне передавать аргумент, я просто использую «умолчание». Язык C++ является довольно сложным языком, который многие обходят стороной. И по моему мнению, сложность вызвана не столько сложностью языковых конструкций, которые есть в языке (типичный программист может с ними никогда не столкнуться), сколько тем, что язык заставляет очень много думать: освободил ли я память, не дорого ли использовать здесь эту функцию и т.п.

Многие программисты (C, C++ и прочие) с недоверием и страхом относятся к тому C++, который стал проявляться после 2011 года. Я слышал немало критики, что язык становится сложнее, что писать на нём теперь могут только «гуру» и т.п. Лично я считаю, что это не так - комитет наоборот много времени уделяет тому, чтобы язык стал дружелюбнее к новичкам и чтобы программистам меньше нужно было думать над особенностями языка. Ведь если нам не нужно бороться с языком, то остаётся время подумать над задачей. К этим упрощениями я отношу и умные указатели, и лямбда-функции и многое другое, что появилось в языке. При этом я не отрицаю того факта, что изучать теперь нужно больше, но что плохого в учении? Или в других популярных языках не происходит изменений, которые нужно изучать?

Дальше, я не сомневаюсь, что найдутся снобы, которые могут сказать в ответ: «Думать не хочется? Иди тогда на PHP пиши». Таким людям я даже отвечать не хочу. Приведу лишь пример из игровой действительности: в первой части Starcraft, когда новый рабочий создаётся в здании, то чтобы он начал добывать минералы (или газ), нужно было вручную его туда послать. Более того, у каждой пачки минералов был лимит, при достижении которого наращивание рабочих было бесполезным, и они даже могли мешать друг другу, ухудшая добычу. В Starcraft 2 это изменили: рабочие автоматически начинают добывать минералы (или газ), а также указывается сколько рабочих сейчас добывают и сколько лимит этого месторождения. Это очень сильно упростило взаимодействие игрока с базой, позволив ему сосредоточиться на более важных аспектах игры: построение базы, накопления войск и уничтожение противника. Казалось бы, это просто отличное нововведение, но что началось в сети! Люди (кто они?) начали визжать, что игра «оказуаливается» и «они убили Starcraft». Очевидно, что такие сообщения могли исходить только от «хранителей тайного знания» и «адептов высокого APM», которым нравилось находиться в неком «элитном» клубе.

Так вот, возвращаясь к нашей теме, чем меньше мне нужно думать над тем, как мне писать код, тем больше мне остаётся времени на то, чтобы думать над решением непосредственной задачи. Думать над тем, какой метод мне использовать - ППСК или ППЗ - ни на йоту не приближает меня к решению задачи, поэтому думать над такими вещами я просто отказываюсь и выбираю один вариант: передача по ссылке на константу. Почему? Потому что я не вижу никаких преимуществ у ППЗ в общих случаях, а частные случаи нужно рассматривать отдельно.

Частный случай, он на то и частный, что заметив то, что в каком-то методе ППСК оказывается узким местом, и, изменив передачу на ППЗ, мы получим важный прирост в производительности, я не задумываюсь применю ППЗ. Но по умолчанию я буду применять ППСК как в обычных функциях, так и в конструкторах. И по возможности буду пропагандировать именно этот способ везде, где только можно. Почему? Потому что считаю практику пропаганды ППЗ порочной из-за того, что львиная доля программистов не слишком сведущи (либо в принципе, либо ещё просто не вошли в курс дела), и они просто следуют советам. Плюс, если есть несколько противоречащих друг друг советов, то они выбирают тот, что попроще, а это приводит к тому, что в коде появляется пессимизация просто потому, что кто-то где-то что-то слышал. Ах да, ещё этот кто-то может привести ссылку на статью Абрахамса, чтобы доказать, что он прав. А ты потом сидишь, читаешь код и думаешь: а вот то, что здесь параметр передаётся по значению, это потому что программист, который это писал, пришёл с Java, просто начитался «умных» статей или тут действительно нужно ППЗ?

ППСК читается куда проще: человек явно знает «хороший тон» C++ и мы идём дальше - взгляд не задерживается. Практика применения ППСК преподавалась программистам C++ годами, какая такая причина от неё отказываться? Это приводит меня к ещё одному выводу: если в интерфейсе метода используется ППЗ, значит там же должен находиться комментарий, почему это именно так. В остальных случаях должна применяться ППСК. Разумеется, есть типы-исключения, но я об этом не упоминаю здесь просто потому, что это подразумевается: string_view , initializer_list , различные итераторы и т.п. Но это исключения, список которых может расширяться в зависимости от того, какие типы используются в проекте. Но суть остаётся неизменной со времён C++98: по умолчанию мы всегда применяем ППСК.

Для std::string разницы на маленьких строках скорее всего не будет, мы поговорим об этом позже.

Параметры в функцию могут передаваться одним из следующих способов:

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

int GetMax(int, int);

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

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

С помощью указателей.

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

double Glue(long& var1, int& var2);

получает две ссылки на переменные типа long и int. При передаче в функцию параметра-ссылки компилятор автоматически передает в функцию адрес переменной, указанной в качестве аргумента. Ставить знак амперсанда перед аргументом в вызове функции не нужно. Например, для предыдущей функции вызов с передачей параметров по ссылке выглядит следующим образом:

Glue(var1, var2);

Пример прототипа функции при передаче параметров через указатель приведен ниже:

void SetNumber(int*, long*);

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

*int Count(int); &int Increase();

возвращают указатель и ссылку соответственно на целочисленную переменную типа int. Следует иметь в виду, что возвращение ссылки или указателя из функции может привести к проблемам, если переменная, на которую делается ссылка, вышла из области видимости. Например,

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

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

const int* FName(int* const Number)

принимает и возвращает указатель на константный объект типа int. Любая попытка модифицировать такой объект в пределах тела вызываемой функции вызовет сообщение компилятора об ошибке. Рассмотрим пример, иллюстрирующий использование константных указателей.

#include

int* const call(int* const);

int X = 13; int* pX = &X; call(pX);

int* const call(int* const x)

//*x++; II нельзя модифицировать объект! return x;

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

const int& FName (const int& Number)

имеющие тот же смысл, что и константные указатели.

#include

const int& call(const int& x)

// нельзя модифицировать объект!

Заранее извиняюсь за пафосную аннотацию про "расстановку точек", но надо же как-то завлечь вас в статью)) Со своей стороны постараюсь, чтобы аннотация все же оправдывала ваши ожидания.

Вкратце о чем речь

Все это и так знают, но все же в начале напомню, как в 1С могут передаваться параметры метода. Передаваться они могут "по ссылке" и "по значению". В первом случае, мы передаем в метод то же самое значение, что и в точке вызова, а во втором - его копию.

По умолчанию в 1С аргументы передаются по ссылке, и изменение параметра внутри метода будет видно извне метода. Здесь дальнейшее понимание вопроса зависит от того, что именно вы понимаете под словом "изменение параметра". Так вот, имеется в виду повторное присваивание и ничего более. Причем, присваивание может быть неявным, например вызовом метода платформы, который возвращает что-то в выходном параметре.

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

Процедура ПоЗначению(Знач Параметр) Параметр = 2; КонецПроцедуры Параметр = 1; ПоЗначению(Параметр); Сообщить(Параметр); // выведет 1

Все работает, как обещано - изменение (а правильнее сказать "замена") значения параметра не приводит к изменению значения вне метода.

Ну а в чем прикол-то?

Интересные моменты начинаются, когда мы начинаем передавать в качестве параметров не примитивные типы (строки, числа, даты и т.п.), а объекты. Вот тут-то и всплывают такие понятия, как "мелкая" и "глубокая" копия объекта, а также указатели (не в терминах C++, а как абстрактные дескрипторы (handles)).

При передаче объекта (например, ТаблицыЗначений) по ссылке, мы передаем само значение указателя (некий handle), который в памяти платформы "держит" объект. При передаче по значению платформа сделает копию этого указателя.

Иными словами, если, передавая объект по ссылке, в методе мы присвоим параметру значение "Массив", то в точке вызова получим массив. Повторное присваивание значения, переданного по ссылке, видно из места вызова.

Процедура ОбработатьЗначение(Параметр) Параметр = Новый Массив; КонецПроцедуры Таблица = Новый ТаблицаЗначений; ОбработатьЗначение(Таблица); Сообщить(ТипЗнч(Таблица)); // выведет Массив

Если же, мы передадим объект по значению, то в точке вызова наша ТаблицаЗначений не пропадет.

Содержимое объекта и его состояние

При передаче по значению копируется не весь объект, а только его указатель. Экземпляр объекта остается одним и тем же. Неважно, как вы передаете объект, по ссылке или по значению - очистка таблицы значений приведет к очистке именно таблицы. Эта очистка будет видна везде, т.к. объект был один-единственный и неважно, как именно он передавался в метод.

Процедура ОбработатьЗначение(Параметр) Параметр.Очистить(); КонецПроцедуры Таблица = Новый ТаблицаЗначений; Таблица.Добавить(); ОбработатьЗначение(Таблица); Сообщить(Таблица.Количество()); // выведет 0

При передаче объектов в методы платформа оперирует указателями (условными, не прямыми аналогами из C++). Если объект передается по ссылке, то ячейка памяти виртуальной машины 1С, в которой лежит данный объект, может быть перезаписана другим объектом. Если объект передается по значению, то указатель копируется и перезапись объекта не приводит к перезаписи ячейки памяти с исходным объектом.

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

И это верно всегда, за исключением...

Клиент-серверное взаимодействие

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

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

  • Явное объявление намерений программиста. Глядя на сигнатуру метода, можно четко сказать, какие параметры входные, а какие выходные. Такой код легче читать и сопровождать
  • Для того, чтобы изменение на сервере параметра "по ссылке" было видно в точке вызова на клиенте, п араметры, передаваемые на сервер по ссылке, платформа обязательно будет сама возвращать на клиента, чтобы обеспечить поведение, описанное в начале статьи. Если параметр не нужно возвращать, то будет перерасход трафика. Для оптимизации обмена данными параметры, значения которых нам не нужны на выходе, нужно помечать словом Знач.

Здесь примечателен второй пункт. Для оптимизации трафика платформа не будет возвращать значение параметра на клиент, если параметр помечен словом Знач. Все это замечательно, но приводит к интересному эффекту.

Как я уже говорил, при передаче объекта на сервер происходит сериализация, т.е. выполняется "глубокая" копия объекта. А при наличии слова Знач объект не поедет с сервера обратно на клиента. Складываем эти два факта и получаем следующее:

&НаСервере Процедура ПоСсылке(Параметр) Параметр.Очистить(); КонецПроцедуры &НаСервере Процедура ПоЗначению(Знач Параметр) Параметр.Очистить(); КонецПроцедуры &НаКлиенте Процедура ПоЗначениюКлиент(Знач Параметр) Параметр.Очистить(); КонецПроцедуры &НаКлиенте Процедура ПроверитьЗнач() Список1= Новый СписокЗначений; Список1.Добавить("привет"); Список2 = Список1.Скопировать(); Список3 = Список1.Скопировать(); // объект копируется полностью, // передается на сервер, потом возвращается. // очистка списка видна в точке вызова ПоСсылке(Список1); // объект копируется полностью, // передается на сервер. Назад не возвращается. // Очистка списка НЕ ВИДНА в точке вызова ПоЗначению(Список2); // копируется только указатель объекта // очистка списка видна в точке вызова ПоЗначениюКлиент(Список3); Сообщить(Список1.Количество()); Сообщить(Список2.Количество()); Сообщить(Список3.Количество()); КонецПроцедуры

Резюме

Если вкратце, то резюмировать можно следующим образом:

  • Передача по ссылке позволяет "затереть" объект совсем другим объектом
  • Передача по значению не позволяет "затереть" объект, но изменения внутреннего состояния объекта будут видны, т.к. идет работа с одним и тем же экземпляром объекта
  • При серверном вызове работа идет с РАЗНЫМИ экземлярами объекта, т.к. выполнялось глубокое копирование. Ключевое слово Знач запретит копирование серверного экземпляра обратно в клиентский, и изменение внутреннего состояния объекта на сервере не приведет к аналогичному изменению на клиенте.

Надеюсь, что этот несложный перечень правил позволит вам легче решать споры с коллегами насчет передачи параметров "по значению" и "по ссылке"

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

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

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

В языке С есть только один способ сопоставления фактических и формальных параметров – передача по значению (передачи параметров по ссылке есть в С++). В Паскале есть передача по значенияю и по ссылке. Бывают и другие методы (в Fortran – копирование-восстановление, в Algol – передача по имени).

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

Метод передачи по значению реализуется следующим способом:

    формальный параметр рассматривается как локальная переменная, так что память для нее выделяется в записи активации вызываемой функции, т.е. в стеке;

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

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

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

void f(int k) { k = -k; } void main() { int i = 1; f(i); printf("i = %d\n", i); // результат: i = 1 }

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

19.3. Передача указателей в функции

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

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

Указатель передается функции так, как и любой другой аргумент – по значению. Понятно, что при передаче адреса параметр следует объявлять как один из типов указателей.

Поскольку функция получает копию аргумента, она не сможет повлиять на сам указатель. Но она может записать все, что угодно туда, куда он направлен, используя для обращения к значению аргумента-оригинала операцию разыменования *.

Задача. Написать функцию для замены местами значений двух переменных и вызвать ее из функции main ().

void swap(int *pa, int *pb) { // параметры-указатели

*pa = *pb; // занести b в a

*pb = temp; // занести a в b

void main(void) {

int i = 10, j = 20;

printf("i и j перед обменом значениями: %d %d\n", i, j);

swap(&i, &j); // передаем адреса переменных i и j

Функция swap() может выполнять обмен значениями двух переменных, на которые указывают pa и pb, потому что в функцию передаются адреса переменных, а не их значения. Внутри функции, используя стандартные операции с указателями, можно получить доступ к содержимому переменных и провести обмен их значений.

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

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

void main(void) {

int i = 10, j = 20;

int *pi = &i, *pj = &j;

printf("i и j перед обменом значениями: %d %d\n", i, j);

swap(pi, pj); // передаем адреса переменных i и j

printf("i и j после обмена значениями: %d %d\n", i, j);

Здесь мы работаем с указателями как с обычными переменными – засылаем в них значения с помощью оператора присваивания, а потом передаем функции.

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

Задача. Написать две функции для вычисления суммы двух отрицательных чисел и их вызов из функции main (). Исходные данные должны вводиться в функции main (). Первая функция должна возвращать заданную величину. Во второй функции обеспечить контроль правильности исходных данных. Функция, кроме вычисления заданной величины, должна возвращать признак правильности исходных данных.

int sum1(int a, int b) {

int sum2(int a, int b, int *sum) {

if (a >= 0 || b >= 0)

return 0; // признак неверных данных

return 1; // признак правильных данных

void main(void) {

scanf(“%d %d”, &x, &y);

printf("Сумма 1 = %d\n", sum1(x,y));

if (sum2(x,y,&s) == 1)

printf("Сумма 2 = %d\n", s);

printf("Неверные данные!\n");