C# — Возвращает значение. ключевое слово return


Содержание

Возвращение нескольких значений с помощью «out» ключевого слова в С#

В настоящее время я пытаюсь понять, что это значит, когда он заявил, что с ключевым словом ‘out’ мы можем вернуть несколько значений. Например, с сайта msdn (https://msdn.microsoft.com/en-us/library/ee332485.aspx): «. Следующие примеры используют для возврата трех переменных с помощью одного вызова метода».

Я не уверен, что я просто не читаю описание правильно, но кажется, что метод() фактически не возвращает (не использует ключевое слово «return») вообще и в основном присваивает поля (аналогично путем прохождения по ссылке). Это согласуется с другими источниками, где они заявляют, что использование «out» может возвращать несколько значений. Не понимаю ли я контекст возвращаемого слова или это что-то вроде того, что я не понимаю концепцию правильно?

c# terminology keyword return out

3 ответа

6 Решение Jurjen [2020-04-07 15:34:00]

Метод действительно не возвращает значение, как вы заметили правильно. Оба ref и out работают со ссылками.

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

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

ref — они имеют те же возможности, что и опорные параметры C++, и

out параметров — это позволяет передачу данных обратно из метода, но не в метод.

Я также предлагаю вам прочитать этот ответ SO и соответствующий блог Джона Скита о передаче параметров. Это даст вам много информации о концепции. И как отмечает Джон Скит, будьте осторожны при использовании ref и out :

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

3 Jinlye [2020-04-07 15:44:00]

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

Если вы хотите получить несколько значений из своего метода, у вас есть несколько вариантов:

Возвращает class или struct — это одно, но может содержать несколько значений (свойства этой вещи)

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

Так что это немного зависит от того, что вы подразумеваете под возвращением. В смысле возвращаемого значения ваш примерный код ничего не возвращает (он объявлен с типом возвращаемого значения void ), и если вы изменили его на тип возвращаемого значения, он мог бы как можно больше вернуть одну вещь в качестве возвращаемого значения. Но в том смысле, говоря вызывающий код о том, что было сделано в рамках методы, то с out параметров вы можете сделать это для нескольких значений.

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

Семантически, когда вы используете ключевое слово out , вы указываете две вещи:

  1. Параметр НЕ используется для ввода.
  2. Значение параметра будет определенно назначено до возвращения метода.

В этом отношении параметр out является значением, которое возвращается из метода.

Компилятор применяет эту семантику. Следующий код генерирует пару предупреждений:

Ошибка CS0177 Параметр out ‘x’ должен быть назначен, прежде чем элемент управления покинет текущий метод

Ошибка CS0269 Использование параметра непризнанного out ‘x’

Обратите внимание, что С# 7 (ака Visual Studio 2020) позволит вам объявлять переменные в вызове метода.

С# 7 позволяет сделать это:

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

Также в С# 7 вы можете использовать кортежи вместо out , что, я думаю, намного лучше (и работает намного лучше с lambdas!).

Приведенный выше пример можно переписать в С# 7 следующим образом:

Методы в языке C#

Терминологические заморочки

В языке С, дедушке языка C#, все было очень просто: данные это числа, строки, массивы; программный код – операторы языка и функции. Функции в C, говоря языком структурного программирования, это подпрограммы.

С переходом от императивного языка C к объектно-ориентированному языку C++ понятие функции (как подпрограммы) сохранилось, однако каждая функция стала принадлежать какому-либо классу. Отметим что в С++ были добавлены специальные функции – конструкторы и деструкторы.

В язык C#, являющийся прямым наследником языка C++, многое перенесено без особых изменений, однако введенная терминология поначалу может ввести вас в заблуждение.

Разберемся с терминами и понятиями более подробно.

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

Авторы языка определили, что функции-члены — это члены, которые обеспечивают некоторую функциональность для манипулирования данными класса. Они включают методы, свойства, конструкторы, финализаторы (деструкторы в С++), операции и индексаторы.

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

Шаблон объявления метода

[модификаторы] тип_возвращаемого_значения ИмяМетода([параметры])
<
// Тело метода
>
В C# определение метода состоит из указания модификаторов (static, public и т.п., модификаторы не обязательны), типа возвращаемого значения, имени метода и списка параметров в круглых скобках. Далее в фигурных скобках записывается тело метода.
Каждый параметр состоит из имени типа параметра и имени, по которому к нему можно обратиться в теле метода.

Возврат из метода и возврат значения

Если метод возвращает значение, то для указания точки выхода должен использоваться оператор возврата return вместе с возвращаемым значением. Если метод не возвращает ничего, то в качестве типа возврата указывается void (опустить тип возврата невозможно).

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

В целом, возврат из метода может произойти при двух условиях. Во-первых, когда встречается фигурная скобка, закрывающая тело метода. И во-вторых, когда выполняется оператор return. Имеются две формы оператора return: одна — для методов типа void (возврат из метода), т.е. тех методов, которые не возвращают значения, а другая — для методов, возвращающих конкретные значения.

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

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

Параметры объявляются в скобках после имени метода. Синтаксис объявления параметров такой же, как и у переменных. А областью действия параметров является тело метода. За исключением особых случаев передачи аргументов методу, параметры действуют так же, как и любые другие переменные.

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


Если переменная, указанная в списке параметров, относится к типу значений (int, double, bool и т.п.), то вызываемый метод получает копию этой переменной, а это значит, что все изменения в ней, выполненные внутри метода будут утеряны.

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

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

Передача структуры (struct) как параметра через ее имя также произойдет через копирование (помните, структура относится к типу значений).

Если же для каких-либо типов данных необходима передача по ссылке (она более эффективна при большом объеме данных, который приходится копировать), то перед указанием типа параметра достаточно указать модификатор ref (reference – ссылка, англ.).

Рассмотрим три примера.

ПРИМЕР 1. В статье Первая программа на языке Си шарп мы разбирали программу:

В классе Program объявляется метод Main() (главная функция приложения), при выполнении которого вызываются два метода класса Console: WriteLine() (вывод строки текста) и ReadKey() (чтение символа с клавиатуры).

Ключевое слово static означает, что данный метод принадлежит классу Program.

Модификатор void указывает, что метод не возвращает никаких параметров.

Метод Main() в качестве списка параметров имеет массив строк, метод WriteLine() имеет один параметр – строку, метод ReadKey() параметров не имеет.

ПРИМЕР 2. Объявление метода в примере Вычисление функции sin(x) выглядит так:

Параметром (аргументом) функции объявляется переменная x типа double, метод возвращает результат также типа double (последний оператор метода return s;). Метод является статическим. Для вызова метода достаточно написать оператор y=Sin2(x);

ПРИМЕР 3. Передача параметров по значению, ссылке и через статическую переменную

Объявим в классе Program структуру West с двумя полями, статическую переменную f и метод vp( ):
static void vp(int[] b, West u, string t, double d, ref decimal y)
где b – массив, u – структура, t – строка, d – вещественное число, y – десятичный тип, передается по ссылке (модификатор ref).

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

Напечатаем эти значения до и после вызова метода vp().

Результат:

Обратите внимание, что второй элемент массива a[1], десятичное число x (передаваемое через ссылку — ref) и статическая переменная f изменились, а структура u, строка t и вещественное число d не изменились.

Отметим, что статическое поле f, объявленное в классе, доступно методам этого класса без механизма передачи параметров.

Некоторые итоги:

1) переменные, объявленные внутри метода или в списке его параметров, являются локальными переменными;

2) при вызове метода параметры из списка в заголовке метода либо копируются в локальные переменные метода, либо передаются по ссылке;

3) копируются все встроенные типы значений, структуры и (по сути) строки, хотя и они относятся к ссылочным типам;

4) по ссылке передаются массивы и другие объекты. Если их поля будут изменены внутри вызываемого метода, то эти изменения сохранятся при возврате в вызывающий метод;

5) передачу параметров по ссылке (без копирования в локальные переменные метода) можно обеспечить, используя модификатор ref. Однако это не имеет смысла для переменных ссылочного типа («масло масленое»);

6) нет необходимости передавать статические переменные класса через список параметров, в рамках одного класса они имеют смысл глобальных переменных в императивных языках программирования (хотя термин «глобальная переменная» в C# не употребляют);

7) для доступа к данным-членам и функциям-членам других классов необходимо указать имя класса, поставить точку, указать имя члена класса, например: Math.PI — константа, число пи=3,14158… , Math.Tan(x) — тангенс числа x, заданного в радианах;

8) доступ к членам других классов зависит от уровня его приватности (public, но не private или protected).

Как и во многих языках программирования для реализации рекурсивных алгоритмов в C# можно использовать рекурсивные методы.

Завершим раздел рассмотрением двух из трех ключевых принципов ООП — наследования и полиморфизма, считаю принцип инкапсуляции (объединение данных и и методов в класс) уже достаточно ясным.

Я получаю сообщение об ошибке «возвращает void, ключевое слово return не должно сопровождаться выражением объекта»

Я выделил приведенный ниже код, который дает мне эту ошибку. Я хочу сделать это, если значение nserved меньше или равно 4, а затем возвращает значение, если оно больше 4, тогда значение по умолчанию равно 0.

P.S. Я давно не начал программировать, и я стараюсь, чтобы мой код был как можно более простым. Спасибо за помощь.

Создан 11 ноя. 13 2013-11-11 20:02:19 user2980503

Ваш метод подписи ‘общественного недействительными setNumberPeople (интермедиат nserved)’, вы ничего не можете вернуться из этого метода, вы можете только ‘возврат ; ‘ – Habib 11 ноя. 13 2013-11-11 20:04:07

Метод, имя которого начинается с’ set’, но которое возвращает значение, является необычным шаблоном. Ваше намерение создать минимальный и максимальный для ‘NumberServed’? – Ian McLaird 11 ноя. 13 2013-11-11 20:10:44

Похоже, вы пытаетесь повторно реализовать свойства (‘get’ /’ set’) — подумайте о том, чтобы использовать встроенные свойства, а не изобретать свои собственные. Также попробуйте использовать соглашения об именах по умолчанию для C# для общего кода: имена функций должны начинаться с заглавной буквы ‘SetFoodCost’, старайтесь избегать частичных слов (например, f-слов в вашем примере :)). – Alexei Levenkov 11 ноя. 13 2013-11-11 20:27:28

3 ответа

Ваш метод подпись public void setNumberPeople(int nserved) который означает, что он не возвращается по существу ничего, или, по крайней мере, не пригодных для использования данных.

Если вы хотите вернуть целое число, изменить эту подпись:

public int setNumberPeople(int nserved) .

Создан 11 ноя. 13 2013-11-11 20:05:51 Michael J. Gray

Рассмотрим использовать обычные свойства с геттеры/сеттеры вместо:

Создан 11 ноя. 13 2013-11-11 20:31:53 Alexei Levenkov

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

Это многословно код, хотя и может быть более четко выражена как:

Наконец, весь класс выглядит, как вы знаете, Java и пишут Java для компилятора C#. C# поддерживает свойства первого класса, и все это можно переписать в виде:

Создан 11 ноя. 13 2013-11-11 20:36:45 David Arno

ВОЗВРАЩЕНИЕ ЗНАЧЕНИЯ ФУНКЦИЕЙ: ОПЕРАТОР return

ВОЗВРАЩЕНИЕ ЗНАЧЕНИЯ ФУНКЦИЕЙ: ОПЕРАТОР return


Создадим функцию, вычисляющую абсолютную величину числа. Абсолютная величина числа — это его значение (если отбросить знак). Следовательно, абсолютная величина 5 равна 5, а абсолютная величина -3 равна 3. Мы назовем эту функцию abs( ). Входом для abs() может быть любое число, для которого мы хотим найти абсолютную величину. Выходом функции будет соответствующее неотрицательное число. Входная величина может обрабатываться благодаря наличию аргумента; выходная величина возвращается (т. е. выдается), как вы увидите ниже, при помощи ключевого слова языка Си — return. Поскольку функция abs( ) должна быть вызвана другой функцией, мы создадим простую программу main( ), основной целью которой будет проверка, работает ли функция abs( ). Программа, спроектированная для того, чтобы проверять работу функции именно таким образом, называется «драйвером». Драйвер подвергает функцию последовательным проверкам. Если результаты оказываются удовлетворительными, то ее можно поместить в программу, заслуживающую большего внимания. (Термин «драйвер» обычно относится к программам, управляющим работой устройств.) Приведем далее наш драйвер и функцию, вычисляющую абсолютную величину числа:

int а = 10, b = 0, с = -22;

printf(» °%d %d %d » , d, e, f);

/* функция, вычисляющая величину числа */

Похожие главы из других книг

9.10 Оператор Return

9.10 Оператор Return Возврат из функции в вызывающеую программу осуществляеся с помощью оператора return, имеющего один из двух видов:return ; return выражение ;Первый может использоваться только в функциях, не возвращающих значения, т.е. в функциях с типом возвращаемого значения void.

6.5.11. Действие RETURN

6.5.11. Действие RETURN Действие RETURN прекращает движение пакета по текущей цепочке правил и производит возврат в вызывающую цепочку, если текущая цепочка была вложенной, или, если текущая цепочка лежит на самом верхнем уровне (например INPUT), то к пакету будет применена политика

Проблема “return void”

Проблема “return void” Посмотрим внимательнее на реализацию функции operator() в нашем адаптере. Что будет, если мы захотим в качестве типа возвращаемого значения функции использовать void? Наша функция запишется так: void operator() . С точки зрения стандарта все хорошо, но все в

1. Пустые значения (Empty-значения)

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

2. Неопределенные значения ( Null-значения)

2. Неопределенные значения (Null-значения) Слово Null используется для обозначения неопределенных значений в базах данных.Чтобы лучше понять, какие значения понимаются под неопределенными, рассмотрим таблицу, являющуюся фрагментом базы данных: Итак, неопределенное

1. Оператор Select – базовый оператор языка структурированных запросов

1. Оператор Select – базовый оператор языка структурированных запросов Центральное место в языке структурированных запросов SQL занимает оператор Select, с помощью которого реализуется самая востребованная операция при работе с базами данных – запросы.Оператор Select

R.6.6.3 Оператор return

R.6.6.3 Оператор return Возврат из функции в обратившуюся к ней функцию происходит с помощью оператора return.Оператор return без выражения можно использовать только в функциях, которые не возвращают значение, т.е. в функциях, возвращающих значение типа void, или в конструкторах (§R.12.1)

Получение констант с определением ошибки функцией LoadStr

Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete

Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete Операторы new и delete с размещением встречаются в C++ не слишком часто, поэтому в том, что вы с ними не знакомы, нет ничего страшного. Вспомните (правила 16 и 17), что когда вы пишете такое

return — Выходит из функции или возвращает ее значение

return — Выходит из функции или возвращает ее значение returnВыходит из функции или возвращает ее значениеСинтаксис:return; return expression;Аргументы:Описание:При вызове функции можно передавать ей одно или более значений (параметров или аргументов), участвующих в выполнении.

Работа с функцией Format

Работа с функцией Format Функция Format, имеющаяся в VBA 5 и VBA 6,- невероятно гибкая. Она применима практически к любым типам данных и имеет настраиваемый вывод- если ни один из встроенных форматов не подойдет, вы можете создать свой, какой пожелаете.В упрощенной форме синтаксис

15.5. Написание оператора, не являющегося функцией-членом

Оператор возврата return

Оператор возврата return Синтаксис:return [ ];Действие:Оператор возврата return заканчивает выполнение функции, в которой он содержится, и возвращает управление в вызывающую функцию. Управление передается в точку вызывающей функции, непосредственно следующую за

КЛЮЧЕВОЕ СЛОВО return

КЛЮЧЕВОЕ СЛОВО return Принципы программирования на языке Си основаны на понятии функции. В представленных ранее примерах программирования мы уже воспользовались несколькими функциями: printf( ), scanf( ), getchar( ), putchar( ) и strlen( ). Эти функции являются системными, однако мы

19.5. Проверка значений, возвращаемых функцией

19.5. Проверка значений, возвращаемых функцией Для проверки значения, возвращаемого вызванной функцией, можно воспользоваться кодом завершения последней команды, размешенной непосредственно после функции, которая вызывается из сценария. Например:check it ls_a directory $FILENAME #

7.2.1. Тип возвращаемого функцией значения

7.2.1. Тип возвращаемого функцией значения Тип возвращаемого функцией значения бывает встроенным, как int или double, составным, как int или double*, или определенным пользователем – перечислением или классом. Можно также использовать специальное ключевое слово void, которое

ВИДЕОУРОК №7. Методы

Доступ к полному курсу с учебными материалами и тестированием на 30 дней за 9.99 USD

Что такое методы?

Создание методов

Функции и процедуры

Примеры создания методов

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

Использование сторожевых операторов

Методы с изменяемыми параметрами

Методы с выходными параметрами

Пройдите тестирование по данному уроку. У вас есть три попытки на урок

C# Стартовый
Методы

Количество вопросов: 5
Время на тестирование: 5 минут

Кандидат: <>
Дата сдачи: <>
Правильные ответы: <> из 5
Время, потраченное на тест: <>

Здравствуйте! Тема нашего сегодняшнего урока – методы. Что такое методы? Методы – это специальные конструкции языка C# которые позволяют группировать в себе программные коды. Какие? Мы уже знаем, у нас имеются переменные, условные конструкции, циклы. Метод – это некий контейнер, который может в себе содержать различные программные коды. А зачем это делать? Зачем нам группировать коды? Мы сейчас к этому подойдем. Давайте мы перейдем к следующему слайду и представим, как может работать метод в общем.

Обратите внимание, у нас имеется вот эта странная конструкция. Представьте себе, сто метод – это кибернетический черный ящик. А что такое черные ящики в кибернетике? Это функциональные преобразователи. У которых есть вход, на вход что-то подается. Внутри находится скрытый механизм, потому он и называется черный ящик, потому что мы не знаем, что в нем происходит. Для многих компьютер может являться черным ящиком. На вход мы с клавиатуры что-то подаем, а на экране выводится. Так вот метод представляет собой некий черный ящик, который содержит в себе некий механизм обработки данных. И у методов имеется вход и выход. Ну не у всех методов. Мы сейчас рассматриваем в общем виде. И вот представьте себе, что я беру один байт. Какое здесь лежит значение? 1. И бросаю его на вход этого черного ящика. Этот механизм закрутился, что-то он с ним сделал и на выходе дает мне какое-то значение. 2. И при этом этот ящик, функциональный-преобразователь он имеет какое-то свое имя. Зачем? Потому что у нас может быть много таких блоков с кодом, который мы будем использовать и к этим блокам с кодами мы будем обращаться по имени, и поэтому метод – это некая конструкция языка C#, которая содержит в себе некую функциональность. Мы к этой конструкции можем обращаться из наших программный кодов. Но сейчас мы к этому подойдем. Обратите внимание, метод может в себя что-то принять, переработать, представьте, что здесь можно было сделать? Ну тут скорее всего идет инкремент. Если мы сюда подаем байт, он сразу инкрементировался и выкинули уже увеличенный на 1. Возможно и так, это самый простейший способ предположить, что же здесь происходит. Как бы мне было интересно заглянуть и посмотреть, что же здесь происходит. Давайте одним глазом заглянем в этот ящик. Смотрите, ага, что у нас здесь имеется? Обратите внимание, вот это имя Addition – это имя черного ящика. То есть наш ящик называется Addition. Справа от имени в круглых скобках у нас идет описание этого входа. Мы говорим, что на вход мы можем подать что-то однобайтовое, однобайтовое значение. А слева от имени у нас идет тоже тип, который описывает тип значения, который выйдет вот отсюда. Обратите внимание, вот эта строка называется сигнатурой метода. Сигнатура в нашем случае состоит из имени метода. Имя этого ящика – Addition – сложение. Из параметров, потому что это отверстие символизирует что-то входящее, входное, параметры, или как еще принято называть – аргументы. Это синонимы. Я чаще говорю – аргументы, кто-то говорит параметры. Так, значит мы здесь записываем тип и имя параметра, которые мы должны передать. А вот это? Это тип возвращаемого значения. Что же этот ящик должен вернуть. Забрасываем в него яблоко, на выходе баночка с джемом. На вход поступил килограмм яблок а на выходе литр яблочного джема. Получается, что вот этот ящик занимается тем, что перерабатывает яблоки в варенье. Значит еще раз смотрим мы создаем метод или можем сказать функцию. Мы сейчас разберем эти отличия. Создаем функцию и имени Addition. Смотрите, эта строка называется сигнатурой метода. Сигнатура метода описывает имя метода, его параметры и возвращаемое значение. Но подождите, с третьей спецификации C# в сигнатуру не входит возвращаемое значение метода. Но пока еще будем говорить по старинке, не совсем правильно, не совсем в свете новых спецификаций. Мы пока будем называть всю эту сроку сигнатурой, а потом будем делать поправки. В нашем случае сигнатура включает в себя имя метода, или его еще называют идентификатор, это то что большими буквами написано на ящике. Вот у меня на компьютере написано четыре больших буквы S, O, N, Y. Это параметры, то что я нажимаю на клавиатуре, а вот это то что выводится на экран. Мы создаем метод с именем Addition, который принимает один аргумент типа byte и возвращает значение типа byte. Обратите внимание, далее идет блок операторных скобок – это тело метода. Ага понятно, значит вот этот блок в операторных скобках, он и содержит в себе полезную функциональность. Механизм, настоящий механизм, который сделали инженеры. Во это – то что написано на ящике, вот это – то что мы можем засунуть в эту дырочку, потому что если мы в машину для варенья, миксер, кухонный комбайн вместо яблок забросим подшипники. Что будет с вашим кухонным комбайном? Все ножи поломаются, все ножи покрошатся. Поэтому вот этот параметр описывает какой тип можно передать в метод. Мы здесь видим, будьте осторожны, здесь только gently types, нежные типы и на выходе мы получаем тоже очищенные яблоки или варенье. То есть мы видим, что этот тип описывает а что же на выходе вы получите. Вы не ожидаете что ваши яблоки тут попадут на философский камень и он сделает с них золото, и здесь пойдут золотые кольца. Нет. Ожидайте байт. Засуньте сюда байт в этот миксер и получите на выходе модифицированный байт. Наш метод состоит условно из двух частей. Первое, это сигнатура метода, и его тело. Тело содержит определенный механизм. Это может быть даже 10, 20, 100 строчек кода. Здесь может производится еще какой-то сложный расчет, эластичность спроса по цене или какие-то теоремы, что угодно. Соответственно, если здесь наш метод занимается сложением, то он и должен называться метод-сложение – Addition. Потому что если мы в магазине будем продавать калькулятор и назовем его – чай, или кофе. И сделаем коробочку, на ней напишем «Чай», а внутри будет лежать калькулятор. Это правильно? Все будут думать, что это не калькулятор, а чай. Будут покупать его, приходить домой, распаковывать и разочаровываться, потому что они не получили того продукта, которого хотели. Так же и с методами, им нужно давать такие имена, чтобы эти имена соответствовали роду выполняемой деятельности этого метода. Еще раз смотрим на метод. Метод состоит из двух частей: сигнатура метода и его тело. Сигнатура, сама по себе, состоит еще из трех частей: это имя метода, набор аргументов, параметров, которые говорят, что можно поместить в этот метод, и возвращаемое значение. Но снова же, давайте вспомним новую спецификацию, возвращаемое значение уже не входит в сигнатуру метода. Мы же начинающие, мы можем позволить себе делать такие поправки. Хорошо, давайте теперь посмотрим, раскрутим этот черный ящик и посмотрим что в нем находится. Когда к нам на вход поступает один байт, смотрите, что мы делаем, мы увеличиваем его на 1. Можно было бы просто сделать что? Argument++. Применить инкремент, правда? И если мы сюда на вход подаем 1. Argument – это как переменная, чему она равна? 1! Значит переменная argument будет равна 1. Значит мы к этому параметру добавляем 1. В итоге чему он равен? 2. Дальше, обратите внимание, ниже ключевое слово return – возврат, вернуть. Вы видите, ключевое слово return возвращает значение этого параметра. И чему оно равно? 2. Он выкидывает из себя двойку. Просто? Просто. Смотрите, какие интересные конструкции. А зачем же и где они используются? Подождите, мы как всегда немного рассмотрим, а потом уже будем смотреть их применение. Ну или есть все-таки желание посмотреть, как это выглядит в коде? Ладно, давайте отойдем от плана урока, откроем Visual Studio. Значит, что нам нужно сделать? Создать новый проект. Берем создаем новый проект. И какой мы создаем? Консольный. И даже не будем его переименовывать. И для того чтобы нам создать метод, нам нужно выйти за пределы этих скобок. Обратите внимание, я выделил этот блок. Давайте, мы его выделим традиционно через Shift+Alt+стрелочки. Смотрите, это метод с именем Main, который ничего не возвращает – vo >


На 12 строке в классе Program, мы пока еще не знаем, что такое классы, но мы уже видим, что это некий контейнер, который может содержать в себе пока методы. На 12 строке мы создаем метод с именем Procedure, который ничего не принимает и ничего не возвращает. Давайте посмотрим, вот этот. Мы создаем вот такую разновидность методов. Ничего не принимает, ничего не возвращает. Но мы же знаем, что технически у нас процедур не существует. Почему? Потому что не смотря на то что я говорю что метод ничего не возвращает, он все равно будет возвращать что? Пустоту – void. Давайте еще раз зайдем сюда, посмотрим. F12. Мы видим что это некая пустая структура. Структура – это тоже конструкция языка C#, которая может группировать в себе методы, переменные и много других конструкций. Так вот мы видим, что эта структура в этом случае является пустой. Помните я говорил вам, чтобы вы протянули руку, а я вам – держите ничего. Ну мы будем говорить, что если метод возвращает void – вот такую структуру пустую, то значит он ничего не возвращает. Потому что если я так с вами поступлю, вы скажете что я вам ничего не дал. Еще раз на 12 строке мы с вами создаем метод с именем Procedure, который ничего не принимает, пустые аргументные скобки, и ничего не возвращает. В теле метода мы выводим на экран строку «Hello!» Далее на 17 строке Мы создаем метод Main, который тоже ничего не принимает, ничего не возвращает, помните void – пустота. В теле метода Main на 21 строке мы вызываем метод с 12 строки. Понимаем, что в данном случае что сделает процессор? Он перейдет на 12 строку Выполнит тело и вернется наследующую команду, которая находится за вызовом метода. Правда? И еще раз. На 12 строке мы с вами создаем метод с именем Procedure, который ничего не принимает, и ничего не возвращает. В теле метода мы выводим на экран строку «Hello!» В теле метода Main на 21 строке мы вызываем метод Procedure с 12 строки. Выполнимся? Даже пошагаем. Нажимаем F11. Вот пожалуйста. Куда у нас переход произойдет? На метод. Смотрите, выполнилось, и мы пошли дальше. Обратите внимание, в результате выполнения это программы мы на экране видим строку «Hello!» Значит мы рассмотрели с вами вот такую версию методов, которые ничего не принимают и ничего не возвращают, то есть возвращают пустоту – void, а это нечестно, это не считается. Почему так сделали Microsoft? Об этом мы уже будем говорить на поздних курсах. Уже обсуждать философию языка позже. Сейчас мы должны просто получать удовольствие, и мы с вами должны достичь чего на этом курсе? Легкости восприятия. Чтобы вы не думали, что программирование – это работа. Чтобы для вас это было приятное времяпровождение. Хорошо. Мы идем в следующий пример.

На 12 строке мы создаем метод с именем Function, там была Procedure – процедура, она ничего не возвращала. Процедуры ничего не возвращают, функции что-то возвращают. В C# не принято проводить такого разделения, такой дифференциации, но мы будем пока это делать, чтобы знать. Значит на 12 строке мы создаем метод с именем Function, который ничего не принимает и возвращает строковое значение. Заметьте, еще поправочка, здесь используем ключевое слово static. Оно нам необходимо в данном случае, чтобы вот в такой простой манере вызывать наши методы, которые мы создаем. В дальнейшем, уже на Essential мы уже познакомимся с философией этого метода. Там будет практически пол урока по static? Но пока нас не затруднить ставить перед именем метода. Еще раз на 12 строке мы создаем метод с именем Function, который ничего не принимает, видите, пустые аргументные скобки. И возвращает строковое значение. В теле метода на 14 строке мы возвращаем строковое значение «Hello!» Смотрите, читается как английский язык – return hello. На 17 строке мы создаем метод Main. Ну не мы его создавали, он был создан по шаблону. Который снова же ничего не принимает и ничего не возвращает. И в теле метода на 22 строке мы создаем строковую локальную переменную. Почему я говорю, что это локальная переменная? Потому что переменные, которые создаются в методах, их принято называть локальными. Потому что мы можем создавать переменные и вот здесь, внутри классов. Но такие переменные мы уже будем называть полями. Мы пока к ним не подходим. И вот такие переменные на языке C# принято называть полями, либо глобальными переменными. Но в языке С++ понятие глобальной переменной – это совсем другое, у нас такого нет, потом будем обсуждать. Но пока мы будем называть все переменные, которые внутри методов локальными. Значит на 22 строке мы создаем строковую локальную переменную с именем @string и присваиваем ей возвращаемое значение function, которое представляет собой строковое значение “Hello”. Поэтому поле выполнения данного метода у нас переменная string будет равна «Hello!» И на 24 строке мы выводим на экран значение строковой локальной переменной @string. Мы говорили, что так не хорошо делать. Ну у нас пример абстрактный, образный. Так, значит какую мы здесь разновидность разобрали? Давайте вернемся к слайду, ничего не принимает и что-то возвращает. То есть это у же была функция. Этот вариант мы рассмотрели. Следующий пример.

Смотрим, на 9 строке мы создаем метод с именем Function, который ничего не принимает и возвращает строковое значение. В теле метода на 11 строке мы создаем строковую локальную переменную с именем word и присваиваем ей значение «Hello!» И на 13 строке мы возвращаем, используя оператор return. Мы возвращаем значение переменной word. На 18 строке в теле метода Main мы создаем строковую локальную переменную с именем word. Смотрите, они одинаковы, но это ничего не значит. Почему? Потому что это разные области видимости. Отсюда мы не увидим что здесь, а отсюда мы не увидим что здесь. Коллизии нет. Этот word принадлежит методу Function, а другой word – методу Main. Обратите внимание, у нас в разных аудиториях могут находится два человека с одинаковым именем. В одной находится Александр, и в другой находится Александр. Конфликта нет. Правда? Разные области видимости. И еще раз на 18 строке мы создаем строковую локальную переменную с именем word и присваиваем ей возвращаемое значение метода Function, которое представляет собой строковое значение «Hello!» И поэтому после выполнения этого метода переменной word будет присвоено что? Строка «Hello!» И на 20 строке мы выводим на экран значение переменной word. Мы на экран выводим «Hello!» Так, здесь какой у нас метод? Ничего не принимает и возвращает что-то. Опять мы его рассмотрели. Хорошо. Идем дальше. Четвертый пример.

На 13 строке мы создаем метод с именем Function, который принимает один строковой аргумент с именем Name, принимает один строковой аргумент или строковой параметр – это синонимы. С именем Name – это имя параметра, имя аргумента. Зачем? Чтобы мы смогли использовать его внутри. Еще раз на 13 строке мы создаем метод с именем Function, который принимает один строковой аргумент с именем name и возвращает строковое значение. В теле метода на 15 строке мы создаем строковую локальную переменную с именем sentence – переводится как? Предложение. И присваиваем ей конкатенацию двух строк и значения аргумента. На 15 строке мы создаем строковую переменную sentense и присваиваем ей конкатенацию двух строк и аргумента. И на 17 строке мы возвращаем значение sentense. На 25 строке мы создаем строковую локальную переменную string. Они совпадают но не конфликтуют, потому что разные области видимости . На 25 строке мы создаем строковую локальную переменную sentence и присваиваем ей возвращаемое значение метода Function, которому качестве аргумента передаём строковое значение Alex. И метод Function вернет конкатенацию строки Hello аргумента Alex и восклицательного знака. На 27 строке мы выводим: «Hello Alex!» Просто? Просто. Подождите, а мы не посмотрели тип. Тип этого метода. Он принимает параметры и возвращает значение. Смотрите, он и принимает параметры и возвращает значение, то есть относится к функции. Смотрим дальше. Следующий пример.

Вы у себя тоже держите открытую студию с примерами и пытайтесь что-то изменять. Экспериментируйте, что-то добавляйте, что-то убирайте. Смотрите на ошибки, которые возникают, просто ваша задача сейчас как можно больше работать с кодом, не боятся экспериментировать, никто не выскочит и не укусит. Максимум, может что-то появится в Error List. А я вам говорил, что в нем у нас всегда будет что-то появляться, потому что такого не бывает, чтобы программист программировал и у него не было ошибок. Это нормально. Главное, чтобы мы могли правильно и своевременно, главное своевременно устранять. Смотрите, что мы здесь видим. Мы здесь видим какой-то очень сложный комментарий. Что же это такое? Как его сделать? Показываю. У метода Main такого комментария нет. Его сделать очень просто. Ставим перед методом курсор и делаем тройной слеш. И он сам формируется. Удаляем. Давайте вот здесь удалим. Раз, два, три. Нам осталось только заполнить эти пустые поля, которые нам подготовились. Смотрите, здесь говорится описание метода, что он будет делать. Давайте посмотрим, как мы его заполнили. Он занимается сложением двух целых чисел. Смотрим первое. Что такое первый аргумент? Summand1 – первое слагаемое. Второй – второе слагаемое. Третье – сумма. Вы скажете в чем же здесь удобство. А очень просто. Когда мы описываем такими комментариями я думаю что мы уже все видели MSDN и формат описания конструкций на MSDN, и тех же самых методов, которые мы изучаем. Каждый метод, что-то выполняет. Иногда нам не достаточно для полного понимания одного имени. Не всегда мы можем назвать коротким словом метод так, чтобы он полностью выражал род деятельности этого метода. И потому нас иногда все-таки приходится комментировать, и более того, нам иногда приходится документировать код, потому что программисты не просто сидят и что-то пишут, они еще и большую часть времени занимаются документированием кода. И вот есть такая программа SandCastle – переводится как песочный замок. Она натравливается на наш программный код, выбирает все вот эти комментарии и формирует из них уже готовый красивый документ с описанием частей, методов, классов, с описанием частей наших программ. Поэтому нам иногда удобно использовать тройные комментарии. Конечно же правила рефакторинга говорят, что ваш код должен быть самодокументируемым. Что это значит? Это значит, что мы должны называть все сущности, в том числе переменные, метод, чтобы не писать, что этот метод складывает что-то. Этот аргумент – это первое слагаемое, второе слагаемое. Мы их правильно называем, чтобы мы могли просто правильно его прочитать, Add – сложение. Summand1, summand2 – первое слагаемое, второе слагаемое. Правда? И нам не надо даже читать комментарии, для того чтобы понять что делает этот метод. Но к сожалению такое не всегда происходит. Программисты так говорят не потому что все так делают. Все стараются следовать этому правилу но мы знаем, что законы частенько нарушаются. Почему? Либо по зависящим, либо по независящим от нас причинам. Так же и программном коде. Не всегда работают те идеальные правила, которые рекомендуют нам теоретики компьютерных наук и вот в том числе разработчики программных языков и того же рефакторинга. Хорошо. Значит мы смотрим здесь на 15 строке мы создаем метод с именем Add, который принимает два целочисленных аргумента. С именами summand1 и aummand2. Возвращает целочисленное значение. И в теле метода мы возвращаем сумму аргументов. В теле метода Main на 22 строке мы создаем две локальных целочисленных переменных summand1 и summand2 – это не те же самые, разные области видимости. И присваиваем им значения 2 и 3 соответственно. На 24 строке мы создаем переменную с именем sum и присваиваем ей возвращаемое значение метода Add, которому в качестве аргументов передаем значения переменных summand1 и summand2. В результате метод Add вернет нам что? Сумму значений аргументов. 2+3=5. В результате выполнения это программы переменной sum присвоится возвращаемое значение метода Add – 5. Далее на 26 строке мы уже используя эту конструкцию с форматированием, что мы с вами изучали мы выведем на экран 2+3=5. Просто? Просто. Значит что мы здесь видим? Процедура? Функция? Конечно же функция. Давайте найдем ее. Она и принимает, и возвращает. Они и принимает один или несколько аргументов, и возвращает какое-то значение. Мы постоянно должны себе узнавать. Вот эта табличка показывает все возможные вариации создания методов. Других нет. Переходим к следующему примеру.

Смотрим, на 11 строке мы создаем метод с именем And, который принимает два булевых аргумента и возвращает булево значение. Иногда в программировании методы возвращающие булево значение принято еще сленгово называть предикатами. Людей, с которыми мы дружим приято называть друзьями, людей, которые нас учат, принято называть учителями или тренерами. I am your trainer. You my students. Смотрите, на 11 строке мы создаем метода, который принимает два логических аргумента и возвращает логическое значение. Вот такие методы иногда принято называть предикатами. Почему мы не будем вдаваться в эту философию в похожесть, в связь с настоящими предикатами из логики. Вы помните, что мы изучали предикаты, ну кто изучал. Возможно. На 11 строке мы создаем метод с именем And – И, похоже на союз И – конъюнкция, которая принимает два булевых аргумента а и b и возвращает булево значение. И в теле метода на 13 строке мы возвращаем возвращаемое значение оператора конъюнкции, производимое над двумя операндами. Над двумя конъюнктами. Видите, да? А И B. Если а будет равно true, а b будет равно false, что мы получим? False! Помните таблицы истинности. Мы true получим только в одном случае, только когда оба конъюнкта и а, и b будут равны истине. То есть true. Во всех остальных случаях мы получим false. А если я сделаю так: два вертикальных слеша и так оставим… Стоп! Надо что сделать? Переименовать метод. Почему? Имя метода не соответствует логике выполняемых действий этого метода. И как его переименовать? Or. Поэтому и здесь нам придётся переименовать Or. Теперь уже более правильно. Хорошо. И мы заходим в тело метода Main. На 18 строке мы создаем два булевых операнда. Operand1 и operand2. Присваиваем им значения tru и true соответственно. На 20 строке мы булевой переменной result присваиваем возвращаемое значение метода And, которому в качестве аргументов передаем значение операндов operand1 и operand2. Мы видим, сто они равны true и true соответственно. Поэтому метод And очевидно вернет true. Ну и далее мы организовываем вывод, чтобы оттенить работу нашей программы. Хорошо. Мы идем дальше.

Обратите внимание, если раньше у нас метод возвращал только одно значение. А у нас имеются сложные условные конструкции и мы можем вернуть либо true, либо false в зависимости от каких-то внутренних условий, которые встречаются внутри метода. Смотрим, на 9 строке мы создаем метод с именем Compare – сравнить, который принимает два интовых аргумента, два целочисленных аргумента: value1 и value2. И метод возвращает строковое значение. В теле метода на 11 строке мы создаем условную конструкцию if, в условии которой указываем выражение – value1 value2. И если это условие удовлетворяет истинности, то мы входим тело этой второй конструкции if и возвращаем строку «Comparison Greater Then». Если же и это условие не удовлетворяет истинности, если value1 не меньше value2, если value1 не меньше value2, то мы понимаем, что четвертого не дано. Мы понимаем, что они равны. И поэтому мы возвращаем «Comparison Equal». Видим, да? Что мы здесь видим? Мы видим три оператора return, которые что-то возвращают. Мы понимаем, что какой-то один из них должен выполнится. А давайте попробуем взять и закомментировать последнее. Ради интереса. Смотрите, ошибка! Так, что это такое? Смотрите, не возвращает значения. Почему? А потому что может произойти такая ситуация, что value1 и не меньше и не больше и что нам тогда делать? Нам надо что-то вернуть. Метод обязан что-то вернуть, потому что в его сигнатуре мы задекларировали возвращаемое значение. И автомат, парсящий этот код, но понимает, что выйдя из этой конструкции, возможно программист забыл что-то вернуть и в итоге у нас не будет возвращаемого значения. А это может что? Ввести в смуту программиста, который использует этот метод. Потому что мы понимаем, что не все мы делаем, мы этим пользуемся. Если я делаю автомобили, то не факт, что у меня есть водительские права и я вообще умею их водить. Если я там работаю фрезеровщиком или токарем на автомобильном заводе. Поэтому, одни программисты могут создавать методы, а другие их могут использовать. И вот представьте, что вы разрабатываете какую-то свою программу и в итоге говорите: «Александр, а можете вот мне создать такой-то метод, который будет мне вычислять что-то?» Например, рассчитывать эластичность спроса по цене. Я говорю, что нет не могу, я же не экономист. А что мне делать? Может посоветуете кого? Да, у меня есть знакомый экономист-программист. Он вам поможет. Может он мне сделать этот метод, а я его угощу пивом? Я говорю, что хорошо, я узнаю. Звоню ему и говорю: «Ваня, можешь помочь хорошему человеку сделать метод? – Да, хорошо сделаю.» Я вам его отдаю, вы смотрите и вообще не понимаете, что внутри этого метода делается, правда? Но тем не менее вы можете им пользоваться. Правда? Потому что он будет выдавать адекватные результаты в соответствии с задаваемыми в этот метод параметрами. Хорошо. Мы уже поняли опасность. И далее в теле метода Main на 25 строке мы создаем два целочисленных операнда operand1 и operand2 и присваиваем им значение 1 и 2. На 27 строке мы строковой переменной result присваиваем возвращаемое значение метода Compare. Которому в качестве аргументов передаем значение операндов 1 и 2 соответственно. Было б хорошо теперь узнать – а что я здесь получу, что выведется на экране? Так я не могу сказать. Надо шагать. Что-то здесь очень запутанно для меня. Давайте пошагаем. Помогайте мне. Так, operand1 = 1, operand2 = 2. Здесь value1 = 1, value2 = 2. Так, 1 меньше 2. Что мне вернет? True. Теперь я вижу и без вашей помощи, что у меня вот вернется эта строка. Давайте пошагаем. Смотрите, все, мы вышли из метода, только сработал первый return. Мы уже не проходим вот этих проверок и тем более не идем на 20 строку. Так хорошо, чему равно? «Comparison Less Then» — вот что мы выведем на экран. Замечательно. А мы переходим дальше.

Вы видите, какие полезные и простые конструкции языка – методы. И давайте посмотрим еще один маленький пример. Помните, Дейкстру мы с вами рассматривали. Математик Дейкстра, который ругал goto, который придумал цикл, то есть алгоритм, который назван его именем. Эго алгоритм расширили. Как он называется? Паук. Если забыли – повторите. Давайте посмотрим, здесь тоже нечто похожее. Мы видим здесь какие-то сторожевые операторы, какие-то номинальные варианты. Давайте ее почитаем. Чтобы понять программу нужно ее прочитать. Чтобы понять что нам хотят сказать люди в своем письме, это письмо нужно прочитать. И как вы быстро читаете скорочтением письма, бегло читаете, так и со временем вы научитесь читать программы Вы должны научится постоянно менять фокус. Посмотреть в общем, потом выделить участок. Его прочитать. Не старайтесь все время читать линейно. Не старайтесь. Сразу посмотрели в общем. Вот у нас тут Function, Main, ага понятно. А что делает Function. Так, тут не понятно, а дальше понятно. То есть переводите фокус, работайте с кодом, как в жизни. Вы же не сканируете меня попиксельно, когда заходите в аудиторию, или когда мы с вами встречаемся. Вы смотрите через маленькую щелочку и сканируете меня, а потом досканировали до глаза и забыли, что вы там сканировали выше. Правда? Мы же не сканер. Мы – люди, и вот так же должны смотреть и на программные коды. Тем более вы видите, какие они простые и приятные. На 9 строке мы создаем метод с именем function, который принимает один строковой аргумент с именем name и возвращает строковое значение. В теле метода на 13 строке Мы создаем условную конструкцию if в которой организовываем проверку. Так, что у нас такое fool. Давайте проверим. О, дурачок, глупец. Плохое слово, нельзя такое слово пропускать, правда? И мы должны отфильтровать такие слова из условно нецензурной, ненормативной лексики. Хоть мы их хотим отфильтровать, хоть раз нам придётся их написать. Либо другой способ. Одна девочка говорит, что она не будет писать. Ну тогда остается написать все остальные, правильные слова, которые не входят в словарь ненормативной лексики. Поэтому придется набраться смелости даже тем, кто не употребляет эти слова и его один раз написать с закрытыми глазами. На 13 строке Мы создаем условную конструкцию if в которой проверяем, если значение аргумента равно слову «Дурачок», то мы прекращаем выполнение нашего метода и возвращаем строку «Вы использовали недопустимое слово.» Мы не можем так называть друг друга. А если же здесь идет имя, например, Александр, то на 20 строке мы создаем строковую локальную переменную с именем sentence и присваиваем значение ей конкатенацию двух строк и значение аргумента. «Hello Alexander!» И на 22 строке мы возвращаем уже сформированную строку. Вы видите что мы здесь поставили. Мы здесь поставили некую сторожевую конструкцию, для того, чтобы случайно компьютер мне не сказал «Hello fool!» Но это неприятно, даже когда чат-боты, вы все наверное сталкивались с чат-ботами, интересное направление, нейросети… Когда чат-бот вам отвечает грубо. Вроде вы и понимаете, что это программа, но вы все равно понимаете, что кто-то туже эту логику занес. Кто-то научил его так здороваться. Правда? Если он груб с вами или использует ненормативную лексику, то вам не хочется оставаться в сеансе пользования этой программы. Хорошо. Значит мы видим, что мы должны поставить некие сторожевые операторы, чтобы все таки не пропустить, какие-то возможно неправильные значение в область так называемого номинального варианта. Настоящей чистой логики. Она может оказаться просто незащищенной. Смотрим дальше на 27 строке мы создаем строковую локальную переменную sentense и пытаемся ей присвоить «Привет дурачок!» Мы присваиваем значение ей возвращаемое значение метода Function, которому в качестве аргумента передаем строковое значение fool – дурачок. Что у нас произойдет? Давайте попробуем пошагать. Я честно говоря запутался здесь немного. Давайте попробуем прошагать. Так смотрим. Name = fool — Вы использовали недопустимое слово. Поэтому меня программа обнотифаевает о том, что я использовал неправильное слово. Но если я попытаюсь ввести здесь Alex и сейчас мы пошагаем, я просто хочу проверить как работает эта программа. Подождите секундочку. Не обращайте внимания. Я сейчас быстро проверю и мы продолжим с вами дальше. Name = Alex. Alex не равно fool. Мы обходим сторожевой оператор, он нас пропустил и здесь мы возвращаем конкатенацию Hello, Alex, ! Возвращаем Hello Alex! Мы возвращаем приветствие в строковую переменную и выводим ее на экран. Все вроде бы правильно. Хорошо. Я закончил. Смотрим дальше. Какие у вас есть вопросы? Здесь все очень просто. Пошли дальше. Мы идем дальше по коду. И мы сейчас с вами к еще одной интересной особенности при работе с методами. Но прежде чем рассматривать этот код дальше хотелось бы вернутся в презентацию и внести кое-какие поправки. Не ругайте меня. Здесь этого не показали. Почему? Чтобы не смущать с первого раза. Значит в информатике у методов может быть две разновидности параметров. Это in параметры, которые входят внутрь. Давайте попробуем это как-нибудь пометить. Это во-первых, in параметры. Синее это in. Дальше. Также бывают out параметры. Сейчас нарисуем еще одну стрелочку красную, чтобы было видно. И вот такие параметры, которые выскакивают из метода. Представляете, не только через вот это отверстие могут выходить возвращаемые значения, через return. Но и через вот это отверстие тоже. А мы из пометим с вами как out. Но что делает Microsoft. Она говорит, что мы перед своими параметрами не будем указывать ключевого слова in. И так понятно, что это входящие параметры. Все аргументы, которые мы с вами рассматривали до этого момента они по умолчанию и так были in. Поэтому это слово писать и не обязательно. Но у нас, как мы видим, существует еще одна разновидность параметров – это out. In в английском языке – это в – внутрь, а out – это из. Но и так уже есть один… А я хочу, чтобы мой метод. Больше возвращал значений. Например 2 – строку и число. Или две разные строки, три разных числа. Мне нужно, чтобы из моего метода выходило больше параметров, больше возвращаемых значений. Чувствуете, что для этого нужно делать. Вот это мы с вами и рассмотрим. Еще маленькая поправочка. Что сделали Microsoft? Они сказали, что если в обычной информатике есть такие понятия как int и out, то у нас будет немножко по другому. In – и так понятно. Мы не будем придумывать ключевого слова. А вот out мы сделаем двух типов. Чистый out и второй – это ref. Видите, что у нас получается. Microsoft берет и как-бы расщепляет вот этот out и делает два различных out. И вот чем вот эти out отличаются мы сейчас с вами и посмотрим. То есть главное поймите, что мы можем создавать такие параметры, которые будут являться одновременно и in, мы их сможем здесь использовать. Вот мы их приняли, и еще сможем что-то вернуть. Принимаем его внутрь. Перерабатываем и возвращаем. Видите, вот такая некая петля получается. Замечательно. Думаю, что здесь предельно понятно. In параметры и так идут по умолчанию. А out параметры, мы сейчас до них дойдем. В нашем понятии, out – как собирательное понятие этих всех out параметров, выходных параметров через область аргументов. Microsoft разбивает и на out, и на ref. Это очень просто. Даже не переживайте. Смотрим сейчас в программный код.

На 12 строке мы создаем метод с именем Method, который принимает один целочисленный аргумент с именем а, помеченный параметром ref. Давайте сейчас смотреть как это работает, и возвращает целочисленное значение. В теле метода мы создаем локальную переменную b, и присваиваем ей произведение аргумента а на 2. Далее, на 15 строке мы меняем значение этого аргумента. Мы присвоили ему значение 5. И на 16 строке мы возвращаем значение b. Казалось бы все просто. Смотрим, на 21 строке мы создаем локальную переменную operand и присваиваем ей значение 2. На 23 строке мы переменной result присваиваем возвращаемое значение метода Method, при этом в качестве аргумента передаем ему этот операнд. То есть мы передвем что? Двоечку. Значит двойка попадает сюда. Мы здесь видим, на 14 строке мы b присваиваем 2*2=4. На 15 строке мы а заменяем. Она была равна чему? Двойке. И возвращаем значение b. Хорошо. Лучше всего – пошагать. Давайте пошагаем. Смотрим. Operand = 2. Идем на метод. А = 2. Так заходим, смотрим. Если а равна 2. Operand чему равен? 2. А = 5. Возвращаем 4. А уже равна 5. Operand – пока еще неизвестно. Интересно, поменяет метод его значение, или не поменяет. Смотрите, operand стал равен 5. Представляете, вот этому методу удалось изменить, удалось каким-то образом побежать в другой метод и изменить его значение. Благодаря чему? Благодаря тому, что у нас здесь используется это ключевое слово ref. И теперь у нас произошел вот такой эффект. Вы скажете, что будет если убрать ключевое слово ref. Хорошо, мы сейчас попробуем убрать его. Но мы сначала довыполним программу. Видите? Операнд стал равен 5. А result стал равен 4. Видим, что сюда как раз и подставился этот операнд вместо 0. А в 1 у нас подставился result. Видите, что произошло? Хорошо. Давайте попробуем теперь взять и удалить ref. Что же у нас сейчас произойдет? Выполняемся. Мы не можем. Мы забыли вот где удалить. Смотрите, ref используется парно, как в самом методе, так и при его вызове. Например описании аргументов метода, так и при вызове этого метода. Сейчас сработало. Но что мы видим? Мы видим, что operand как был 2, он так и остался равен двум. Методу не удалось изменить значение вот этого внешнего операнда, который передавался в качестве аргумента метода. Вы скажете – как так получилось? Очень просто. Когда мы используем методы без ref, у нас сюда в качестве аргумента метода на самом деле передается не сам аргумент. А что там? А здесь просто его копия. Копия этого операнда. То есть, когда мы передаем в качестве аргументов методов значение каких-то переменных, то не сама переменная сюда передается, а копия значения это переменной. Но если мы хотим отказаться от такого похода, от передачи копий, что нам нужно сделать? Указать ключевое слово ref перед аргументом, но будьте осторожны. Вы видите к какому эффекту это может привести? Это может привести к тому, что изменив значение такого аргумента у себя в теле метода вы поменяете значение какой-то чужой переменной, которая будет передаваться при вызове вашего метода в качестве аргумента. ref отказывается от использования копий передаваемых параметров. Просто? Просто. Если указываем ref, то мы начинаем работать с самим оригиналом. По умолчанию это закрыто, потому что представляете сколько было бы ошибок. Мы передаем, а эта переменная меняется, а мы не хотели бы что бы она менялась. Вы видите что происходит здесь? Значит ключевое слово ref позволяет нам отменить передачу копии значения, оно говорит, что мы будем работать с оригиналом. И поэтому вы видите? Двойное использование. Double using. Как здесь используется ключевое слово ref, так и здесь. Работал какой-то другой программист, который дал вам этот метод. А вот здесь работаете вы. И мы с вами помним, что если мы забудем указать это ключевое слово, то на подчеркнется. Ошибка. Как? А здесь же используется ref. А зачем меня заставляют сюда передавать такое значение, которое нужно поменять. Это же не безопасно. Что это за логика, такая интересная. Я тут храню какие-то свои значения в операнде, а тут мне его собираются изменить. Но в каких-то случаях это требуется для быстроты и для оптимизации. Думаю, что здесь предельно понятно. Но мы с вами помним, что у нас в информатике есть два вида параметров: in и out. Если мы напишем in? то не смотрите, что оно загорелось синим, в языке C# есть такое ключевое слово, но оно не совместимо. Вот во многих языках, например в языке АДА его нужно писать. АДА. Августа Лавлейс первая женщина-программист, которая писала программы для Чарльза Бэббиджа под его несуществующие машины. И мы видим, что в языке АДА нужно указывать такое слово. В языке C# — не нужно. Мы и так подразумеваем, что оно здесь используется если мы не указываем ref или out. Потому что мы уже смотрели уже, что понятие out оно разделяется. И теперь мы видим, что происходит, когда мы используем ref. То практически у нас получается некий возвращаемый параметр. Вот из тела этого метода происходит присвоение значения вот сюда. Чем то напоминает работу return. Только return просто, как из рогатки, выстреливает, а здесь целенаправлено изменится значение переменной, которая будет передана в качестве аргумента метода. Видите, если этот аргумент буде помечен спецификатором ref. Видите да? А мы с вами переходим дальше.

В этом примере мы видим использование еще одного ключевого слова out. Значит мы помним, у нас имеется ref и out. То есть стандартный классический out разделается на ref и out. Давайте почитаем эту программу. На 9 строке мы создаем метод с именем Method, который принимает один целочисленный аргумент, помеченный спецификатором out. Этот метод возвращает целочисленное значение. Обратите внимание, в теле этого метода на 12 строке, мы этому аргументу присваиваем значение 2. И на 13 строке мы возвращаемое произведение этого аргумента и 2. На 18 строке мы создаем локальную целочисленную переменную operand. Мы ее ничем не инициализируем и на чтение мы ее использовать не можем. На 22 строке мы создаем целочисленную локальную переменную result, которой присваиваем возвращаемое значение метода и в качестве аргумента этого метода мы передаем вот эту переменную. Мы же понимаем, что ref\out откажется от построения копий. А тем более здесь и значения никакого нет. Дальше мы заходим в наш метод. Здесь мы ей присваиваем 2. Этот операнд, эта переменная будет сразу равна 2, потому что ей вот здесь присваивается 2. Как? Через вот этот параметр. То есть мы обращаемся вот сюда. Затем идет вызов метода и мы вот сюда ее записываем. Чем же отличается out от ref. Out – это более строгая форма ref, потому что если бы мы здесь указали ref… Давайте просто поменяем и посмотрим. У нас все равно будет ошибка. Мы пытаемся передать непроинициализированную переменную. Уже не работает. Потому что для того что бы все сработало нам нужно присвоить этой переменной значение. Все, теперь работает. Далее. Если мы закомментируем вот здесь и ничего не будем изменять. Выполняемся. У нас все сработало. При этом не смотря на то что мы 2*2 она осталась 2, потому что данный оператор не меняет значения аргумента. То есть вот все сработало. Но если мы с вами откажемся от ref и вернемся к out. Мы видим что во-первых out позволяет использовать непроинициализированные переменные в качестве аргументов, потому что мы все равно гарантированно вот здесь ей что-то присвоим. А давайте попробуем ей ничего не присваивать. Что мы видим? Ошибку. Сразу же появилась ошибка – непроинициализированная переменная. Давайте здесь 2 присвоим. Все равно не хочет выполнятся. Смотрите, что написано. The out parameter ‘a’ must be assigned to before control leaves the current method. Он обязательно должен быть внутри метода как-то проинициализирован. Пусть повторно, но должно быть присвоено значение. Не смотря на то, что сюда поступает нормальная, обычная 2. Это не то что здесь непроинициализированно. Использование оператора out накладывает на нас такие ограничения, как обязательное присвоение аргументу какого-то значения внутри метода, аргументы которого помечены спецификатором out. Какое различие между ref и out? Строгость. В первую очередь это строгость. Если ref позволяет необязательно присваивать. Я могу как изменить, так и не изменять. То с out меня обязывают изменять. Потому что когда я использую ваш метод, я ставлю скобку и мне сразу показывается out. Я понимаю, что это возвращаемое значение через блок аргументов, что сюда, то что я передам в итоге оно перезапишется, потому что метод что то мне хочет выдать. А зачем такое сделано? Когда мы раньше работали с API функциями, то там очень часто использовался такой подход с передачей возвращаемых значений через аргументы. Почему? Потому что функции имели очень много параметров и все API функции возвращали нам успех, либо не успех выполнения этой функции. API – Application Programming Interface. Это просто набор уже готовых функций, которые находятся в библиотеках. Возможно вы встречали такие библиотеки на своем диске. Это kernel32.dll, user32.dll. Вот там где находятся как раз API функции, которые и используются для работы с ОС Windows. И каждая из этих функций должна была нам возвращать успешно ли она выполнилась, все ли хорошо сработало. Либо не успешно. А возвращаемые значения мы очень часто получали как раз именно через параметры. Поэтому возвращаемое значение как мы понимаем может быть не одно, не два. Представьте что мы возвращаем координаты разные. Нам предлагается вернуть либо структуру с этими координатами. Но все равно такой подход остался. Может быть он не совсем идеальный и не совсем удобный. Но он остался и его нужно знать. И сегодня тоже используется такой подход в библиотеках Microsoft, потому что мы знаем, что библиотеки Microsoft предоставляют нам очень много готовой функциональности, она вся сконцентрирована в специальных программный библиотеках, которые установлены у вас на компьютере. Эти библиотеки еще называются знакомым нам именем – фреймворк – каркас. Но мы не будем сейчас заострять внимание на непонятных вещах. Мы сейчас изучаем только основы. А к этому мы подойдем немножко позже. А мы с вами переходим дальше и смотрим следующий пример.

На 11 строке мы создаем метод с именем Method, который принимает один аргумент с именем а. И при чем он помечен спецификатором out. Почему я говорю спецификатором. Многие могут сказать – модификатором. Но модификатор должен что-то модифицировать, изменять. Модно назвать его модификатором. Но в каких-то случаях такие конструкции называют спецификаторами. В теле метода Method мы на 14 строке аргументу а присваиваем значение 1. И на 16 возвращаем 2. Здесь нам предлагается закомментировать. Как вы думаете, если я закомментирую? Уже будет ошибка, потому что у нас имеется некий constraint. Некое ограничение при работе с out параметрами. Потому что если бы здесь был ref, я программисту говорю что бы он был осторожен возможно я что-то передам. Но если я использую out, я говорю, что программист, я обязательно здесь что-то передам. Если я вдруг забуду это сделать. То компилятор мне напомнит. The out parameter ‘a’ must be assigned to before control leaves the current method. Он говорит, что обязательно должен передавать. И я сразу знаю, что что-то я здесь забыл. Ага я забыл сработать с этим параметром. И на 21 строке мы создаем переменную operand типа int. Заметьте, при использовании out мы можем себе позволить оставить эту переменную непроинициализированной. Конечно, это изначально плохой подход – оставлять локальные переменные непроинициализированными, потому что им по умолчанию не присваивается значение по умолчанию. Автоматически им ничего не присваивается, потому что если бы эта переменная находилась вот здесь, внутри класса. Видите, она находится в области классов за пределами методов. Она называлась бы полем и ей автоматом присвоилось бы значение по умолчанию – 0. Но для локальный переменных такого не происходит. Поэтому не оставляйте непроинициализированные переменные. Это может привести к временным неприятным эффектам вот время разработки. Это будет вас раздражать. Это же не сложно взять и поставить 0. Ну уже о правилах разработки, о правилах правильного написания кода вы будете смотреть и изучать на курсе рефакторинга. Что это такое вы можете посмотреть первые пробные уроки и соответственно узнать хотя бы основы. Что это такое. На 25 строке мы создаем целочисленную переменную result и присваиваем ей возвращаемое значение метода Method. И после выполнения чему же будет равен этот операнд. Шагать будем? Нет уже не будем. Он будет равен 1. Operand = 1, а result? 2. А мы переходим дальше.

Смотрим. Что происходит здесь? На 8 строке мы создаем метод с именем AddTwo, который принимает один целочисленный аргумент и ничего не возвращает. Обратите внимание, мы в этом методе параметру а присваиваем сумму этого параметра и двух. Метод и называется – прибавить двойку. Мы к этому параметру всякий раз будем прибавлять двойку в любом месте, где мы вызовем этот метод. И выводим на экран значение а. Чему оно будет равно? 2. На 20 строке мы предлагаем пользователю ввести некое число. На 21 строке мы преобразовываем число. Помните мы увеличивали число. Говорили, что любое число состоит из точек. Это просто картинка. Я не могу сложить два альбомных листа, на которых написано один, а на другом 2. Это что? 1+2 = 12. Нет! Мне нужно переконвертировать это во что? В нейро-код, и там как-то выполнить это. А мы здесь делаем то же самое. Мы берем строку. Мы ожидаем, что пользователь введет действительное число, а не какую-то абрукадабру. Мы здесь уже не ставим защиту от неправильного ввода. Ее еще называют защитой от дурака. Значит на 21 строке мы конвертируем строковое представление числа в целочисленный формат. Видите мы используем Int32. На нем Parse – преобразовать. И он его преобразовывает и возвращает преобразованное из строки число в целочисленный формат. На 23 строке мы его выводим. На 26 строке мы вызываем метод AddTwo И в качестве аргумента передаем значение переменной result. И смотрим на 27 строке выводим его значение. Обратите внимание, что сюда передается? Сюда передается не сам аргумент, а его копия. Сюда оно копируется. Если я введу двойку, то она попадает сюда и мы ведем работу с копией. Я думаю, вы это запомнили. Этот пример, он в другой форме повторяет, все сказанное нами ранее. Почему? Потому что многие программисты могут забыть о существовании или о правилах использования таких ключевых слов, спецификаторов, модификаторов как ref и out. А сейчас по умолчанию какой параметр? In. А почему вы не пишите? А в языке C# не надо писать ключевое слово in. Мы и так понимаем, что это только для входа. А если мы здесь напишем ref, то он будет и in, они все будут in, они все входят, но и будут обладать особенностями out. Ref – это необязательный out. Программист! Просто будь осторожен, если я здесь ставлю ref. Возможно я поменяю значение твоей переменной напрямую, которую ты передаешь сюда. А если же я поставлю out, то я говорю – Программист! Обязательно мне что-то передай, обязательно. Потому что это настоящее возвращаемое значение через этот блок аргументов. Хорошо. А мы переходим дальше. Заходим в следующий. Что ми видим здесь? Мы видим тот же самый код, что и в предыдущем примере.

Только что изменилось? Здесь в одном месте добавилось ref. Нет не в одном. Ref и out используются парно. Давайте посмотрим где же его пара? Вот идет пара. А зачем парно? Затем, что мы знаем, что частенько методы и классы разрабатывают одни программисты, а используют другие. И это для того, чтобы когда программист-пользователь забыл поставить это ключевое слово, и тут произойдет изменение этого result. Он будет долго анализировать свою программу и не понимать как же оно так получилось. Придется ему долго шагать. Промышленные программы они не такие простые, как мы рассматриваем, потому там может уйти очень много времени на отладку программы на ее анализ и поиск ошибок. Поэтому, если программист пользователь этого метода забудет указать ключевое слово ref – ему будет ошибка. Ага, мы забыли указать ключевое слово ref. И когда я это вижу, тогда разработчик метода, его даже не видно, он где-то там в библиотеках, значит он мне говорит, чтобы я был осторожным, может случится так, что я поменяю значение этой переменной, оно изменится, потому что я получаю к ней доступ напрямую, я работаю не с копией. Я тогда открываю и начинаю смотреть что же делает этот метод. Читать документацию. Чужой код нам редко приходится читать, это считается плохим стилем, когда начинаем читать чужой код. Нам тогда на работе говорят, что мы на работу приходим что делать? Работать, а не учится. И тем более перечитывать повторно код за другими программистами. Для этого есть тестировщики, которые находят ошибки. Если вы считаете что код работает некорректно обратитесь в отдел тестирования и скажите, что вот этот метод мне неадекватный результат выдает, либо я не могу научится им пользоваться. И уже в отделе тестирования скажут либо так, либо так. В отделе качества. Хорошо. А мы с вами продолжаем. Мы все с вами обыгрываем. Почему мы так остановились на ref и out. Потому что он используется не так часто, метко но редко. Нам важно запомнить. А что это за пример?

На 7 строке мы создаем метод с именем Add, который принимает три целочисленный аргумента х, у, sum. Смотрим, Первый помечен ref, второй тоже, третий ничем не помечен. На 9 строке мы аргументу sum присваиваем сумму двух первых аргументов. Здесь нам предлагается ввести первое число. Давайте попробуем выполнится. Проверить как она работает. Я говорю 2, второе число – 3. Это должно быть 5. Так, подождите, почему 0. Вот мы и попали с вами в ошибку. Теперь нам с вами придется сидеть и перечитывать код. Давайте перечитывать и вникать в чем же здесь проблема. Метод у нас не сработал, работает некорректно, приходится изучать коды. Так, ввели первое число, конвертировали. Ввели второе число. Конвертировали тоже нормально. Вызываем метод Add, так передаем первый summand1, передаем второй summand2, что мы ввели. Передаем sum. Здесь логическая ошибка. Что нудно сделать для корректной работы этой программы. Вы скажете, что программист напихал ref вот сюда, хотя они здесь сто лет не нужны, мы не собираемся менять операнды, забыл поставить ref вот сюда, раз он уже выбрал такой подход для работы. Убираем здесь ref, убираем здесь ref, нужно еще сюда ref поставить, потому что эти модификаторы используются парно. Выполняемся и проверяем. Вводим 2 и 3. Наконец-то все сработало. Но, обратите внимание, смесь двух логик. Эту логику делал один программист. А другую логику, тот что использовал первую. Получается как. То что мы с вами изменили, мы влезли в чужой код. Этого делать нельзя. Возвращаемся. Получается что кто здесь неправильно создал программу? Этот неправильно воспользовался ею, а это неправильно создал. Уже сейчас учитесь находить ошибки неправильного стиля. Это специально сделано, что мы могли проанализировать, ну зачем мы помечаем ref эти два аргумента? Неужели мы собираемся менять значение операндов, которые вводит пользователь? Нет! Ну допустим, да мы хотим вернуть сумму. Идеально было бы сделать вот так, организовать как возвращаемое значение метода Add и здесь указать return. Ну видите? Теперь что вам нужно сделать? Вам нужно дома попробовать после урока сесть и привести эту программу к красивому виду, как бы вы это сделали и проанализируйте все некрасивые моменты этого кода. А мы переходим дальше в последний пример.

Почему мы показываем код не совсем корректный, потому что вы часто будете встречаться с такими кодами, потому что множество разных людей, у каждого свой уровень компетенции, у каждого свой уровень квалификации, компетенции. Если человек пишет не совсем красивый код с точки зрения общепринятых правил, ну ничего страшного, может он еще не доучился. Мы видим, что он учится, он старается, он над собой работает. И иногда нужно подсказывать, помогать и не совсем так строго судить. Мы смотрим дальше на 7 строке мы создаем метод Add и вот здесь мы указываем out и вот здесь я вижу уже более правильно, но что вам не нравится. Вы скажете, что зачем вам сюда, зачем summand помечаются как ref. Вот снова у нас идет анализ этого кода. А вот так уже это более красиво. И здесь мы могли бы вернуть успешность либо неуспешность выполнения данной операции. Выполняем какой-то сложный расчет, можем даже провести маленький тестик и сказать, что да действительно, тест прошел успешно и вернуть значение true или false в зависимости от того, как выполнился этот метод. Но мы уже не используем таких подходов с возвращаемыми значениями успешного, либо не успешного результата операции. Это раньше так было. Это было вынужденное решение, потому что были сотни и тысячи методов в библиотеках, это были очень низкоуровневые методы и с ними было работать не сложно, запутанно. Были очень толстые книги, которые описывали работу этих методов. Еще не были так развиты интернет ресурсы, как сегодня. Нельзя было быстро и легко найти информацию. Приходилось покупать много литературы. Делится ей друг с другом и делится друг с другом информацией в живую. Не то что не было интернета. Он был. Не было столько ресурсов, не было столько полезной информации как сегодня. Хорошо. И мы с вами закончили рассматривать методы. Мы достаточно много уделили времени рассмотрению этих конструкций. Давайте еще раз зайдем напоследок и посмотрим вот этот слайд. Запомните его. Какие у нас имеются разновидности методов, формы использования методов. Вы уже видите вот эти стрелочки вверх. Давайте я их здесь напоследок пририсую. Вот эти стрелочки, которые говорят о том, что параметры могут быть как входные, так и входно-выходные. Он одновременно и входит, и выходит. И выходные параметры бывают необязательны и обязательны. На этом наш урок закончен, спасибо за внимание. До новых встреч.

Урок №12. Функции и оператор возврата return

Обновл. 23 Июл 2020 |

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

Функции

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

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

Функция, в которой находится вызов, называется caller, а функция, которую вызывают — вызываемая функция.

Результат выполнения программы выше:

Starting main()
In doPrint()
Ending main()

Эта программа начинает выполнение с первой строчки функции main(), где выводится на экран следующая строчка: Starting main() . Вторая строчка функции main() вызывает функцию doPrint(). На этом этапе выполнение стейтментов в функции main() приостанавливается и процессор переходит к выполнению стейтментов внутри функции doPrint(). Первая (и единственная) строчка в doPrint() выводит текст In doPrint() . Когда процессор завершает выполнение doPrint(), он возвращается обратно в main() к той точке, на которой остановился. Следовательно, следующем стейтментом является вывод строки Ending main() .

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

Правило: Не забывайте указывать круглые скобки () при вызове функций.

Возвращаемые значения

Когда функция main() завершает своё выполнение, она возвращает целочисленное значение обратно в операционную систему, используя оператор return.

Функции, которые мы пишем, также могут возвращать значения. Для этого нужно указать тип возвращаемого значения (или ещё «тип возврата»). Он указывается при объявлении функции, перед её именем. Обратите внимание, тип возврата не указывает, какое именно значение будет возвращаться. Он указывает только тип этого значения.

Затем, внутри вызываемой функции, мы используем оператор return, чтобы указать возвращаемое значение: какое именно значение будет возвращаться обратно в caller.

Рассмотрим простую функцию, которая возвращает целочисленное значение:

Результат выполнения программы выше:

Первый вызов функции return7() возвращает 7 обратно в caller, которое затем передаётся в std::cout для вывода.

Второй вызов функции return7() опять возвращает 7 обратно в caller. Выражение 7 + 3 производит результат 10 , который затем выводится на экран.

Третий вызов функции return7() опять возвращает 7 обратно в caller. Однако main() ничего с ним не делает, поэтому ничего и не происходит (возвращаемое значение игнорируется).

Примечание: Возвращаемые значения не выводятся на экран, если их не передать объекту std::cout. В последнем вызове функции return7() значение не отправляется в std::cout, поэтому ничего и не происходит.

Тип возврата void

Функции могут и не возвращать значения. Чтобы сообщить компилятору, что функция не возвращает значение, нужно использовать тип возврата void. Взглянем ещё раз на функцию doPrint() из примера выше:

Эта функция имеет тип возврата void, который означает, что функция не возвращает значения. Поскольку значение не возвращается, то и оператор return не требуется.

Вот ещё один пример использования функции типа void:

В первом вызове функции returnNothing() выводится Hi! , но ничего не возвращается обратно в caller. Точка выполнения возвращается обратно в main(), где программа продолжает своё выполнение.

Второй вызов функции returnNothing() даже не скомпилируется. Функция returnNothing() имеет тип возврата void, который означает, что эта функция не возвращает значения. Однако main() пытается отправить это значение (которое не возвращается) в std::cout для вывода. std::cout не может обработать этот случай, так как значения не вывод не предоставлено. Следовательно, компилятор выдаст ошибку. Вам нужно будет закомментировать эту строку, чтобы компиляция прошла успешно.

Возврат в main()

Теперь у вас есть понимание того, как работает функция main(). Когда программа выполняется, операционная система делает вызов функции main() и начинается её выполнение. Стейтменты в main() выполняются последовательно. В конце функция main() возвращает целочисленное значение (обычно 0 ) обратно в операционную систему. Поэтому main() объявляется как int main() .

Почему нужно возвращать значения обратно в операционную систему? Дело в том, что возвращаемое значение функции main() является кодом состояния, который сообщает операционной системе о том, успешно ли было выполнение программы или нет. Обычно, возвращаемое значение 0 (ноль) означает что, всё прошло успешно, тогда как любое другое значение означает неудачу/ошибку.

Обратите внимание, по стандартам C++ функция main() должна возвращать целочисленное значение. Однако, если вы не укажете return в конце функции main(), то компилятор возвратит 0 автоматически, если никаких ошибок не будет. Но рекомендуется указывать return в конце main() и использовать тип возврата int для функции main().

Детальнее о возвращаемых значениях

Во-первых, если тип возврата функции не vo >0 , если не предоставлено другое значение.

Во-вторых, когда процессор встречает в функции оператор return, он немедленно выполняет возврат значения обратно в caller и точка выполнения также переходит в caller. Любой код, который находится за return-ом в функции — игнорируется.


Функция может возвращать только одно значение через return обратно в caller. Это может быть либо число (например, 7 ), либо значение переменной, либо выражение (которое производит результат), либо определённое значение из набора возможных значений.

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

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

Повторное использование функций

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

Электроника для всех

Блог о электронике

1.4.11. Оператор return

Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом. Функция main передает управление операционной системе. Формат оператора:

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

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

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

int sum (int a, int b)

Функция sum имеет два формальных параметра a и b типа int, и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров.

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

Добавить комментарий Отменить ответ

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

Делегаты и события в .NET

От переводчика. Судя по своему опыту, а также по опыту знакомых коллег-программистов, могу сказать, что для начинающего разработчика среди всех базовых функций языка C# и платформы .NET делегаты и события являются одними из наиболее сложных. Возможно, это из-за того, что необходимость делегатов и событий на первый взгляд кажется неочевидной, или же из-за некоторой путаницы в терминах. Поэтому я решил перевести статью Джона Скита, рассказывающую о делегатах и событиях на самом базовом уровне, «на пальцах». Она идеальна для тех, кто знаком с C#/.NET, однако испытывает затруднение в понимании делегатов и событий.

Представленный здесь перевод является вольным. Однако если под «вольным», как правило, понимают сокращённый перевод, с упущениями, упрощениями и пересказами, то здесь всё наоборот. Данный перевод является немного расширенной, уточнённой и обновлённой версией оригинала. Я выражаю огромную благодарность Сергею Теплякову aka SergeyT, который внёс неоценимый вклад в перевод и оформление данной статьи.

Люди часто испытывают затруднения в понимании различий между событиями и делегатами. И C# ещё больше запутывает ситуацию, так как позволяет объявлять field-like события, которые автоматически преобразуются в переменную делегата с таким же самым именем. Эта статья призвана прояснить данный вопрос. Ещё одним моментом является путаница с термином «делегат», который имеет несколько значений. Иногда его используют для обозначения типа делегата (delegate type), а иногда — для обозначения экземпляра делегата (delegate instance). Чтобы избежать путаницы, я буду явно использовать эти термины — тип делегата и экземпляр делегата, а когда буду использовать слово «делегат» — значит, я говорю о них в самом широком смысле.

Типы делегатов

В каком-то смысле вы можете думать о типе делегата как о некоем интерфейсе, в котором определён лишь один метод с чётко заданной сигнатурой (в этой статье под сигнатурой метода я буду понимать все его входные и выходные (ref и out) параметры, а также возвращаемое значение). Тогда экземпляр делегата — это объект, реализующий этот интерфейс. В этом понимании, имея экземпляр делегата, вы можете вызвать любой существующий метод, сигнатура которого будет совпадать с сигнатурой метода, определённого в «интерфейсе». Делегаты обладают и другой функциональностью, но возможность делать вызовы методов с заранее определёнными сигнатурами — это и есть самая суть делегатов. Экземпляр делегата хранит ссылку (указатель, метку) на целевой метод и, если этот метод является экземплярным, то и ссылку на экземпляр объекта (класса или структуры), в котором «находится» целевой метод.

Тип делегата объявляется при помощи ключевого слова delegate . Типы делегатов могут существовать как самостоятельные сущности, так и быть объявленными внутри классов или структур. Например:

В этом примере объявлены два типа делегата. Первый — DelegateArticle.FirstDelegate , который объявлен на уровне пространства имён. Он «совместим» с любым методом, который имеет один параметр типа int и возвращает значение типа string . Второй — DelegateArticle.Sample.SecondDelegate , который объявлен уже внутри класса и является его членом. Он «совместим» с любым методом, который имеет два параметра типа char и не возвращает ничего, так как возвращаемый тип помечен как void .

Обратите внимание, что оба типа делегата имеют модификатор доступа public . Вообще, по отношению модификаторов доступа типы делегатов ведут себя так же, как классы и структуры. Если для типа делегата явно не указан модификатор доступа и этот тип объявлен внутри пространства имён, то он будет доступен для всех объектов, также находящихся внутри этого пространства имён. Если же тип делегата без модификатора объявлен внутри класса или структуры, то он будет закрытым, аналогично действию модификатора private .

При объявлении типа делегата нельзя использовать модификатор static .

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

Оба типа делегата, объявленные в этом примере, наследуются от System.MulticastDelegate , который, в свою очередь, наследуется от System.Delegate . На практике учитывайте наследование только от MulticastDelegate — различие между Delegate и MulticastDelegate лежит прежде всего в историческом аспекте. Эти различия были существенны в бета-версиях .NET 1.0, но это было неудобно, и Microsoft решила объединить два типа в один. К сожалению, решение было сделано слишком поздно, и когда оно было сделано, делать такое серьёзное изменение, затрагивающее основу .NET, не решились. Поэтому считайте, что Delegate и MulticastDelegate — это одно и то же.

Каждый тип делегата, созданный вами, наследует члены от MulticastDelegate , а именно: один конструктор с параметрами Object и IntPtr , а также три метода: Invoke , BeginInvoke и EndInvoke . К конструктору мы вернёмся чуточку позже. Вообще-то эти три метода не наследуются в прямом смысле, так как их сигнатура для каждого типа делегата своя — она «подстраивается» под сигнатуру метода в объявленном типе делегата. Глядя на пример кода выше, выведем «наследуемые» методы для первого типа делегата FirstDelegate :

Как вы видите, возвращаемый тип методов Invoke и EndInvoke совпадает с таковым, указанным в сигнатуре делегата, так же, как и параметр метода Invoke и первый параметр BeginInvoke . Мы рассмотрим цель метода Invoke далее в статье, а BeginInvoke и EndInvoke рассмотрим в разделе, описывающем продвинутое использование делегатов. Сейчас ещё рано об этом говорить, так как мы ещё даже не знаем, как создавать экземпляры делегатов. Вот об этом и поговорим в следующем разделе.

Экземпляры делегатов: основы

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

Создание экземпляров делегатов

Прежде всего замечу, что эта статья не рассказывает о новых функциональных возможностях C# 2.0 и 3.0, связанных с созданием экземпляров делегатов, так же, как и не покрывает обобщённые делегаты, появившиеся в C# 4.0. Моя отдельная статья о замыканиях «The Beauty of Closures» повествует о новых возможностях делегатов, которые появились в C# 2.0 и 3.0; кроме того, много информации по этой теме содержится в главах 5, 9 и 13 моей книги «C# in Depth». Я буду придерживаться явного стиля создания экземпляров делегатов, который появился в C# 1.0/1.1, так как полагаю, что такой стиль проще для понимания того, что происходит «под капотом». Вот когда вы постигнете основы, то можно будет приступать к освоению новых возможностей из C# 2.0, 3.0 и 4.0; и наоборот, без твёрдого понимания основ, изложенных в этой статье, «новый» функционал делегатов может быть для вас неподъёмным.

Как говорилось ранее, каждый экземпляр делегата обязательно содержит ссылку на целевой метод, который может быть вызван через этот экземпляр делегата, и ссылку на экземпляр объекта (класса или структуры), в котором объявлен целевой метод. Если целевой метод является статическим, то, естественно, ссылка на экземпляр отсутствует. CLR поддерживает и другие, немного различные формы делегатов, где первый аргумент, передаваемый в статический метод, хранится в экземпляре делегата, или же ссылка на целевой экземплярный метод передаётся как аргумент при вызове метода. Более подробно об этом можно прочитать в документации к System.Delegate на MSDN, однако сейчас, на данном этапе, эти дополнительные сведения не существенны.

Итак, мы знаем, что для создания экземпляра нам нужны две «единицы» данных (ну и сам тип делегата, конечно), однако как дать знать об этом компилятору? Мы используем то, что в спецификации к C# называется «выражение создания делегата» (delegate-creation-expression), что является одной из форм new delegate-type (expression). Выражение (expression) должно быть или другим делегатом с таким же самым типом (или с совместимым типом делегата в C# 2.0), или же «группой методов» (method group), которая состоит из названия метода и опциональной ссылки на экземпляр объекта. Группа методов указывается точно также, как и обычный вызов метода, но без каких-либо аргументов и круглых скобок. Необходимость в создании копий делегата возникает довольно редко, поэтому мы сосредоточимся на более общих формах. Примеры ниже.

Конструктор делегата, о котором мы говорили ранее, имеет два параметра — ссылку на вызываемый метод типа System.IntPtr (в документации MSDN этот параметр называется method) и ссылку на экземпляр объекта типа System.Object (в документации MSDN этот параметр называется target), которая принимает значение null, если метод, указанный в параметре method, является статическим.

Необходимо сделать важное замечание: экземпляры делегатов могут ссылаться на методы и экземпляры объектов, которые будут невидимыми (вне области видимости) по отношению к тому месту в коде, где будет произведён вызов экземпляра делегата. Например, при создании экземпляра делегата может быть использован приватный (private) метод, а потом этот экземпляр делегата может быть возвращён из другого, публичного (public) метода или свойства. С другой стороны, экземпляр объекта, указанный при создании экземпляра делегата, может быть объектом, который при вызове будет неизвестным по отношению к тому объекту, в котором был совершен вызов. Важно то, что и метод, и экземпляр объекта должны быть доступны (находиться в области видимости) на момент создания экземпляра делегата. Другими словами, если (и только если) в коде вы можете создать экземпляр определённого объекта и вызвать определённый метод из этого экземпляра, то вы можете использовать этот метод и экземпляр объекта для создания экземпляра делегата. А вот во время вызова ранее созданного экземпляра делегата права доступа и область видимости игнорируются. Кстати, о вызовах…

Вызов экземпляров делегатов

Экземпляры делегатов вызываются таким же образом, как вызываются обычные методы. К примеру, вызов экземпляра делегата d1, тип которого определён в самом верху как delegate string FirstDelegate (int x) , будет следующим:

Метод, ссылку на который хранит экземпляр делегата, вызывается «в рамках» (или «в контексте», если другими словами) экземпляра объекта, если такой есть, после чего возвращается результат. Написание полноценной программы, демонстрирующей работу делегатов, и при этом компактной, не содержащей «лишнего» кода, является непростой задачей. Тем не менее, ниже приведена подобная программа, содержащая один статический и один экземплярный метод. Вызов DelegateTest.StaticMethod эквивалентен вызову StaticMethod — я включил название класса, чтобы сделать пример более понимаемым.

Синтаксис C# по вызову экземпляров делегатов является синтаксическим сахаром, маскирующим вызов метода Invoke , который есть у каждого типа делегата. Делегаты могут выполняться асинхронно, если предоставляют методы BeginInvoke/EndInvoke , но об этом позже.

Комбинирование делегатов

Делегаты могут комбинироваться (объединяться и вычитаться) таким образом, что когда вы вызываете один экземпляр делегата, то вызывается целый набор методов, причём эти методы могут быть из различных экземпляров различных классов. Когда я раньше говорил, что экземпляр делегата хранит ссылки на метод и на экземпляр объекта, я немного упрощал. Это справедливо для тех экземпляров делегатов, которые представляют один метод. Для ясности в дальнейшем я буду называть такие экземпляры делегатов «простыми делегатами» (simple delegate). В противовес им, существуют экземпляры делегатов, которые фактически являются списками простых делегатов, все из которых основываются на одном типе делегата (т.е. имеют одинаковую сигнатуру методов, на которые ссылаются). Такие экземпляры делегатов я буду называть «комбинированными делегатами» (combined delegate). Несколько комбинированных делегатов могут быть скомбинированы между собой, фактически становясь одним большим списком простых делегатов. Список простых делегатов в комбинированном делегате называется «списком вызовов» или «списком действий» (invocation list). Т.о., список вызовов — это список пар ссылок на методы и экземпляры объектов, которые (пары) расположены в порядке вызова.

Важно знать, что экземпляры делегатов всегда неизменяемы (immutable). Каждый раз при объединении экземпляров делегатов (а также при вычитании – это мы рассмотрим чуть ниже) создаётся новый комбинированный делегат. В точности, как и со строками: если вы применяете String.PadLeft к экземпляру строки, то метод не изменяет этот экземпляр, а возвращает новый экземпляр с проделанными изменениями.

Объединение (также встречается термин «сложение») двух экземпляров делегатов обычно производится при помощи оператора сложения, как если бы экземпляры делегатов были числами или строками. Аналогично, вычитание (также встречается термин «удаление») одного экземпляра делегата из другого производится при помощи оператора вычитания. Имейте ввиду, что при вычитании одного комбинированного делегата из другого вычитание производится в рамках списка вызовов. Если в оригинальном (уменьшаемом) списке вызовов нет не одного из тех простых делегатов, которые находятся в вычитаемом списке вызовов, то результатом операции (разностью) будет оригинальный список. В противном случае, если в оригинальном списке присутствуют простые делегаты, присутствующие и в вычитаемом, то в результирующем списке будут отсутствовать лишь последние вхождения простых делегатов. Впрочем, это легче показать на примерах, нежели описать на словах. Но вместо очередного исходного кода я продемонстрирую работу объединения и вычитания на примере нижеследующей таблицы. В ней литералами d1, d2, d3 обозначены простые делегаты. Далее, обозначение [d1, d2, d3] подразумевает комбинированный делегат, который состоит из трёх простых именно в таком порядке, т.е. при вызове сначала будет вызван d1, потом d2, а затем d3. Пустой список вызовов представлен значением null.

Выражение Результат
null + d1 d1
d1 + null d1
d1 + d2 [d1, d2]
d1 + [d2, d3] [d1, d2, d3]
[d1, d2] + [d2, d3] [d1, d2, d2, d3]
[d1, d2] — d1 d2
[d1, d2] — d2 d1
[d1, d2, d1] — d1 [d1, d2]
[d1, d2, d3] — [d1, d2] d3
[d1, d2, d3] — [d2, d1] [d1, d2, d3]
[d1, d2, d3, d1, d2] — [d1, d2] [d1, d2, d3]
[d1, d2] — [d1, d2] null

Кроме оператора сложения, экземпляры делегатов могут объединяться при помощи статического метода Delegate.Combine ; аналогично ему, операция вычитания имеет альтернативу в виде статического метода Delegate.Remove . Вообще говоря, операторы сложения и вычитания — это своеобразный синтаксический сахар, и компилятор C#, встречая их в коде, заменяет на вызовы методов Combine и Remove. И именно потому, что данные методы являются статическими, они легко справляются с null-экземплярами делегатов.

Операторы сложения и вычитания всегда работают как часть операции присваивания d1 += d2 , которая полностью эквивалента выражению d1 = d1+d2 ; то же самое для вычитания. Снова-таки, напоминаю, что экземпляры делегатов, участвующие в сложении и вычитании, не изменяются в процессе операции; в данном примере переменная d1 просто сменит ссылку на новосозданный комбинированный делегат, состоящий из «старого» d1 и d2.

Обратите внимание, что добавление и удаление делегатов происходит с конца списка, поэтому последовательность вызовов x += y; x -= y; эквивалентна пустой операции (переменная x будет содержать неизменный список подписчиков, прим. перев.).

Если сигнатура типа делегата объявлена такой, что возвращает значение (т.е. возвращаемое значение не является void) и «на основе» этого типа создан комбинированный экземпляр делегата, то при его вызове в переменную будет записано возвращаемое значение, «предоставленное» последним простым делегатом в списке вызовов комбинированного делегата.

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


События

Перво-наперво: события (event) не являются экземплярами делегатов. А теперь снова:
События — это НЕ экземпляры делегатов.

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

Я пришел к выводу, что самый лучший способ понять события, это думать о них как о «как бы» свойствах (properties). Свойства, хотя и выглядят «типа как» поля (fields), на самом деле ими определённо не являются — вы можете создать свойства, которые вообще никак не используют поля. Подобным образом ведут себя и события — хотя и выглядят как экземпляры делегатов в плане операций добавления и вычитания, но на самом деле не являются ими.

События являются парами методов, соответствующе «оформленные» в IL (CIL, MSIL) и связанные между собой так, чтобы языковая среда чётко знала, что она «имеет дело» не с «простыми» методами, а с методами, которые представляют события. Методы соответствуют операциям добавления (add) и удаления (remove), каждая из которых принимает один параметр с экземпляром делегата, который имеет тип, одинаковый с типом события. То, что вы будете делать с этими операциями, в значительной степени зависит от вас, но обычно операции добавления и удаления используются для добавления и удаления экземпляров делегатов в/из списка обработчиков события. Когда событие срабатывает (и не важно, что было причиной срабатывания — щелчок по кнопке, таймер или необработанное исключение), происходит поочерёдный (один за другим) вызов обработчиков. Знайте, что в C# вызов обработчиков события не является частью самого события.

Методы добавления и удаления вызываются в C# так eventName += delegateInstance; и так eventName -= delegateInstance; , где eventName может быть указано по ссылке на экземпляр объекта (например, myForm.Click ) или по называнию типа (например, MyClass.SomeEvent ). Впрочем, статические события встречаются довольно редко.

События сами по себе могут быть объявлены двумя способами. Первый способ — с явной (explicit) реализацией методов add и remove; этот способ очень похож на свойства с явно объявленными геттерами (get) и сеттерами (set), но с ключевым словом event. Ниже представлен пример свойства для типа делегата System.EventHandler . Обратите внимание, что в методах add и remove не происходит никаких операций с экземплярами делегатов, которые туда передаются — методы просто выводят в консоль сообщения о том, что они были вызваны. Если вы выполните этот код, то увидите, что метод remove будет вызван, невзирая на то, что мы ему передали для удаления значение null.

Моменты, когда приходится игнорировать полученное значение value , возникают довольно редко. И хотя случаи, когда мы можем игнорировать передаваемое таким образом значение, крайне редки, бывают случаи, когда нам не подойдет использование простой переменной делегата для содержания подписчиков. Например, если класс содержит множество событий, но подписчики будут использовать лишь некоторый из них, мы можем создать ассоциативный массив, в качестве ключа которой будет использовать описание событие, а в качестве значения — делегат с его подписчиками. Именно эта техника используется в Windows Forms — т.е. класс может содержать огромное количество событий без напрасного использования памяти под переменные, которые в большинстве случаев будут равными null.

Field-like события

C# обеспечивает простой способ объявления переменной делегата и события в один и тот же момент. Этот способ называется «field-like событием» (field-like event) и объявляется очень просто — так же, как и «длинная» форма объявления события (приведённая выше), но без «тела» с методами add и remove.

Эта форма создает переменную делегата и событие с одинаковым типом. Доступ к событию определяется в объявлении события с помощью модификатора доступа (т.о., в примере выше создаётся публичное событие), но переменная делегата всегда приватна. Неявное (implicit) тело события разворачивается компилятором во вполне очевидные операции добавления и удаления экземпляров делегата к/из переменной делегата, причём эти действия выполняются под блокировкой (lock). Для C# 1.1 событие MyEvent из примера выше эквивалентно следующему коду:

Это что касается экземплярных членов. Что касается статических событий, то переменная тоже является статической и блокировка захватывается на типе вида typeof(XXX) , где XXX — это имя класса, в котором объявлено статическое событие. Язык C# 2.0 не дает никаких гарантий по поводу того, что используется для захвата блокировок. Он говорит лишь о том, что для блокировки экземплярных событий используется единственный объект, связанный с текущим экземпляром, а для блокировки статических событий — единственный объект, связанный с текущим классом. (Обратите внимание, что это справедливо лишь для событий, объявленных в классах, но не в структурах. Существуют проблемы с блокировками событий в структурах; и на практике я не помню ни одного примера структуры, в которой было объявлено событие.) Но ничего из этого не является таким уж полезным, как вы могли бы подумать, подробности см. в секции о многопоточности.

Итак, что происходит, когда вы в коде ссылаетесь на MyEvent ? Внутри тела самого типа (включая вложенные типы) компилятор генерирует код, который ссылается на переменную делегата ( _myEvent в примере выше). Во всех остальных контекстах компилятор генерирует код, который ссылается на событие.

Какой в этом смысл?

Теперь, когда мы знаем и о делегатах, и о событиях, возникает вполне закономерный вопрос: зачем в языке нужны и те, и другие? Ответ — из-за инкапсуляции. Предположим, что в некоем вымышленном C#/.NET событий не существует. Как тогда сторонний класс может подписаться на событие? Три варианта:

  1. Публичная переменная (поле) с типом делегата.
  2. Приватная переменная (поле) с типом делегата с оболочкой в виде публичного свойства.
  3. Приватная переменная (поле) с типом делегата с публичными методами AddXXXHandler и RemoveXXXHandler.

Вариант №1 ужасен — мы, как правило, ненавидим публичные поля. Вариант №2 немного лучше, но позволяет подписчикам эффективно переопределять (override) один одного — будет слишком лёгким написать выражение someInstance.MyEvent = eventHandler; , в результате которого все существующие обработчики будут заменены на eventHandler , вместо того, чтобы добавить к существующим eventHandler . Плюс к этому, вам всё равно нужно явно прописывать код свойств.

Вариант №3 — это, собственно, то, что и предоставляют вам события, но с гарантированным соглашением (генерируемым компилятором и резервируемым специальными флагами в IL) и со «свободной» реализацией, если только вы будете счастливы от семантики field-like событий. Подписка и отписка к/от событий инкапсулируется без предоставления произвольного доступа к списку обработчиков события, благодаря чему удаётся упростить код для операций подписки и отписки.

Потокобезопасные события

(Примечание: с выходом C# 4 этот раздел несколько устарел. От переводчика: подробнее см. раздел «От переводчика»)

Ранее мы коснулись блокировки (locking), которая происходит в field-like событиях во время операций add и remove, которые автоматически реализуются компилятором. Это делается ради предоставления некой гарантии потокобезопасности (thread safety). К сожалению, оно не столь уж и полезное. Прежде всего, даже в C# 2.0 спецификации позволяют установить блокировку на ссылку к this-объекту или на сам тип в статических событиях. Это противоречит принципам блокировки на приватных ссылках, что необходимо для недопущения взаимных блокировок (deadlock).

По иронии, вторая проблема является полной противоположностью первой — из-за того, что в C# 2.0 вы не можете гарантировать, какая блокировка будет использована, вы сами тоже не можете её использовать, когда вызываете событие, чтобы удостовериться, что вы видите самое новое (актуальное) значение в данном потоке. Вы можете применять блокировку на что-либо другое или воспользоваться специальными методами, работающими с барьерами памяти, но всё это оставляет неприятное послевкусие 1↓ .

Если вы хотите, чтобы ваш код был истинно потокобезопасным, таким, что когда вы вызываете событие, то всегда используете наиболее актуальное значение переменной делегата, а также таким, что вы можете убедиться, что операции add/remove не мешают одна другой, то для достижения такой «железобетонной» потокобезопасности вам необходимо писать тело операций add/remove самому. Пример ниже:

Вы можете использовать единую блокировку для всех ваших событий, и даже использовать эту блокировку для чего-либо ещё — это уже зависит от конкретной ситуации. Обратите внимание, что вам нужно «записать» текущее значение в локальную переменную внутри блокировки (для того, чтобы получить самое актуальное значение), а затем проверить это значение на null и выполнить вне блокировки: удерживание блокировки во время вызова события является очень плохой идеей, легко приводящей к взаимоблокировке. Чтобы объяснить это, представьте, что некий обработчик событий должен дождаться, пока другой поток выполнит какую-то свою работу, и если во время неё этот другой поток вызовет операцию add/remove для вашего события, то вы получите взаимоблокировку.

Вышеприведённый код работает корректно потому, что как только локальной переменной handler будет присвоено значение someEvent, то значение handler уже не изменится даже в том случае, если изменится сам someEvent . Если все обработчики событий отпишутся от события, то список вызовов будет пуст, someEvent станет null, но handler будет хранить своё значение, которое будет таким, каковым оно было на момент присвоения. На самом деле, экземпляры делегатов являются неизменяемыми (immutable), поэтому любые подписчики, подписавшиеся между присваиванием ( handler = someEvent ) и вызовом события ( handler (this, e); ), будут проигнорированы.

Кроме этого, нужно определить, нужна ли вам вообще потокобезопасность. Собираетесь ли вы добавлять и удалять обработчики событий из других потоков? Собираетесь ли вы вызывать события из разных потоков? Если вы полностью контролируете своё приложение, то очень правильным и легким в реализации ответом будет «нет». Если же вы пишете библиотеку классов, то, скорее всего, обеспечение потокопезопасности пригодится. Если же вам определённо не нужна потокобезопасность, то хорошей идеей будет самостоятельно реализовать тело операций add/remove, чтобы они явно не использовали блокировки; ведь, как мы помним, C# при автогенерации этих операций использует «свой» «неправильный» механизм блокировки. В этом случае ваша задача очень проста. Ниже — пример вышеприведённого кода, но без потокобезопасности.

Если на момент вызова метода OnSomeEvent переменная делегата someEvent не содержит списка экземпляров делегатов (вследствие того, что они не были добавлены через метод add или же были удалены через метод remove), то значение этой переменной будет null, и чтобы избежать её вызова с таким значением, и была добавлена проверка на null. Подобную ситуацию можно решить и другим путём. Можно создать экземпляр делегата-заглушку (no-op), который будет привязан к переменной «по умолчанию» и не будет удаляться. В этом случае в методе OnSomeEvent нужно просто получить и вызвать значение переменной делегата. Если «реальные» экземпляры делегатов так и не были добавлены, то будет просто-напросто вызвана заглушка.

Экземпляры делегатов: другие методы

Ранее в статье я показал, что вызов someDelegate(10) — это просто сокращение для вызова someDelegate.Invoke(10) . Кроме Invoke , типы делегатов имеют и асинхронное поведение посредством пары методов BeginInvoke / EndInvoke . В CLI они являются опциональными, но в C# они всегда есть. Они придерживаются той же модели асинхронного выполнения, что и остальная часть .NET, позволяя указывать обработчик обратного вызова (callback handler) вместе с объектом, хранящим информацию о состоянии. В результате асинхронного вызова код выполняется в потоках, созданных системой и находящихся в пуле потоков (thread-pool) .NET.

В первом примере, представленном ниже 2↓ , нет обратных вызовов, здесь просто используются BeginInvoke и EndInvoke в одном потоке. Такой шаблон кода иногда полезен, когда один поток используется для синхронных в целом операций, но вместе с тем содержит элементы, которые могут быть выполнены параллельно. Ради простоты кода все методы в примере статические, но вы, конечно же, можете использовать «асинхронные» делегаты вместе с экземплярными методы, и на практике это будет происходить даже чаще. Метод EndInvoke возвращает то значение, которое возвращается в результате вызова экземпляра делегата. Если во время вызова экземпляра делегата возникнет исключение, то это исключение выбросит и EndInvoke .

Вызовы метода Thread.Sleep вставлены только ради того, чтобы продемонстрировать, что методы CountCharacters и Parse действительно выполняются параллельно с основным потоком. Сон в CountCharacters в 2 секунды достаточно большой для того, чтобы принудить пул потоков выполнить задачи в других потоках — пул потоков сериализует запросы, которые не требуют много времени для выполнения, чтобы таким образом избежать чрезмерного создания новых потоков (создание новых потоков является относительно ресурсоёмкой операцией). «Усыпляя» поток на долгое время, мы таким образом имитируем «тяжелую», требующую много времени на выполнение задачу. А вот и вывод нашей программы:

Если процесс выполнения делегата в стороннем потоке ещё не завершился, то вызов метода EndInvoke в основном потоке будет иметь схожий эффект с вызовом Thread.Join для вручную создаваемых потоков — основной поток будет ждать, пока завершится задача в стороннем потоке. Значение IAsyncResult , которое возвращается методом BeginInvoke и передаётся на вход EndInvoke , может использоваться для передачи состояния из BeginInvoke (через последний параметр — Object state ), однако необходимость в такой передаче при использовании делегатов возникает не часто.

Программный код, приведённый выше, довольно прост, но недостаточно эффективный в сравнении с моделью обратных вызовов (callback model), вызываемых после окончания выполнения делегата. Как правило, именно в методе обратного вызова (callback) происходит вызов метода EndInvoke , возвращающего результат выполнения делегата. Хотя чисто теоретически этот вызов всё так же блокирует основной поток (как в вышеприведённом примере), но на практике блокировки потока не произойдёт, так как метод обратного вызова выполняется лишь тогда, когда делегат выполнил свою задачу. Метод обратного вызова может использовать состояние с некими дополнительными данными, которое будет передано ему из метода BeginInvoke . Пример ниже использует те же самые делегаты для парсинга и подсчёта количества символов в строке, что и в примере выше, но на сей раз с методом обратного вызова, в котором результат выводится на консоль. Параметр state типа Object используется для передачи информации о том, в каком формате выводить результат на консоль, и благодаря этому мы можем использовать один метод обратного вызова DisplayResult для обработки обоих асинхронных вызовов делегатов. Обратите внимание на приведение типа IAsyncResult к типу AsyncResult : значение, принимаемое методом обратного вызова, всегда является экземпляром AsyncResult , и через него мы можем получить оригинальный экземпляр делегата, результат из которого получим, используя EndInvoke . Здесь немного странным является то, что тип AsyncResult объявлен в пространстве имён System.Runtime.Remoting.Messaging (которое вам надо подключить), тогда как все остальные типы объявлены в System или System.Threading .

На этот раз почти вся работа выполняется в потоках из пула потоков. Основной поток просто инициирует асинхронные задачи и «засыпает» до тех пор, пока все эти задачи не выполнятся. Все потоки из пула потоков являются фоновыми (background) потоками, которые не могут «удерживать» приложение (т.е. они не могут предотвратить его закрытие), и чтобы приложение не завершилось до того, как в фоновых потоках завершится работа делегатов, мы и применили вызов Thread.Sleep(3000) в основном потоке — можно надеяться, что 3-х секунд хватит для выполнения и завершения делегатов. Вы можете это проверить, закомментировав строчку Thread.Sleep(3000) — программа завершится почти мгновенно после запуска.

Результат работы нашей программы представлен ниже. Обратите внимание на порядок вывода результатов на консоль — результат работы парсера появился до результата работы счётчика, так как среда не гарантирует сохранение порядка при вызове EndInvoke . В предыдущем примере парсинг завершался значительно быстрее (100 мс), чем счётчик (2 сек), однако основной поток ждал их обоих, чтобы вывести прежде всего результат счётчика, а лишь потом парсера.

Помните, что при использовании данной асинхронной модели вы должны вызвать EndInvoke ; это нужно для того, чтобы гарантировать отсутствие утечек памяти и обработчиков. В некоторых случаях при отсутствии EndInvoke утечек может и не быть, но не надо на это надеяться. Для более детальной информации вы можете обратиться к моей статье «Multi-threading in .NET: Introduction and suggestions», посвящённой многопоточности, в частности, к разделу «The Thread Pool and Asynchronous Methods».

Заключение

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

Примечания

1. Прим. перев. Признаюсь, объяснение Джона Скита довольно невнятное и скомканное. Чтобы детально разобраться, почему блокировка на текущем экземпляре и на типе — это плохо, и почему следует вводить отдельное приватное поле только для чтения, я крайне советую воспользоваться книгой «CLR via C#» за авторством Джеффри Рихтера, которая уже пережила 4 издания. Если говорить о втором издании 2006 года выпуска, переведённое в 2007 на русский язык, то информация о данной проблеме расположена в «Часть 5. Средства CLR» – «Глава 24. Синхронизация потоков» — раздел «Почему же «отличная» идея оказалась такой неудачной».

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

От переводчика

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

Алексей Дубовцев. Делегаты и события (RSDN).
Хотя статья не новая (датируется 2006 годом) и рассматривает лишь основы делегатов и событий, уровень «рассмотрения основ» намного глубже: здесь и более пристальное рассмотрение типа MulticastDelegate, особенно в плане комбинированных делегатов, и описание принципа работы на уровне MSIL, и описание класса EventHandlerList, и многое другое. В общем, если вы хотите рассмотреть основы делегатов и событий на более глубоком уровне, то данная статья определённо для вас.

coffeecupwinner. События .NET в деталях.
Надеюсь, вы обратили внимание на заметку об устаревшем материале вначале раздела «Потокобезопасные события»? В C# 4 внутренняя реализация field-like событий, которую критикуют Скит и Рихтер, была полностью переработана: теперь потокобезопасность реализуется через Interlocked.CompareExchange, безо всяких блокировок. Об этом, в числе прочего, и рассказывает эта статья. Вообще, статья скрупулёзно рассматривает только события, однако на намного более глубоком уровне, чем у Джона Скита.

Daniel Grunwald. andreycha. Слабые события в C#.
Когда говорят о преимуществах между C#/.NET с одной стороны и C++ с другой, то в преимущество первым помимо прочего записывают автоматическую сборку мусора, которая ликвидирует утечки памяти как класс ошибок. Однако не всё так радужно: события могут приводить к утечкам памяти, и именно решению этих проблем посвящена данная очень детальная статья.

rroyter. Зачем нужны делегаты в C#?
Как я упоминал во вступлении, в начинающих разработчиков недопонимания с делегатами связаны с отсутствием видимых причин, требующих их использования. Данная статья очень доходчиво демонстрирует некоторые ситуации, где делегаты будут крайне уместны. Кроме того, здесь продемонстрированы новые возможности делегатов, введённые в C# 2-й и 3-й версий.

10 фич в C#, о которых вы определённо должны узнать и начать их использовать

Если вы только начали изучение C# или же решили расширить свои знания, мы нашли для вас 10 фич, знание которых позволит вам избежать ошибок, писать более понятный код и сохранить кучу времени.

1. async / await

Использование паттернов async / await позволяет разблокировать UI / текущий поток во время выполнения блочных операторов. Паттерны async / await позволяют коду продолжить выполнение, даже если что-то блокирует его выполнение (например, веб-запрос).


2. Инициализаторы объектов / массивов / коллекций

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

3. Лямбды, предикаты, делегаты и замыкания

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

4. ?? (Оператор объединения с NULL)

x ?? y — возвращает x , если значение отличается от null ; в противном случае возвращает y .

Может быть несколько операторов .

?? также может быть использован для перевода типов null в не null :

5. $”” (Интерполяция строк) — C# 6

Фича в C# 6 позволяет эффективно и элегантно собирать строки:

6. ?.(определяет null) — C# 6

x?.y — доступ к членам, определяемый условием null . Возвращает значение null , если левый операнд имеет значение null .

Больше никаких NullReferenceExceptions!

7. Выражение nameof — C# 6

Может показаться, что выражение nameof не особо полезно, но это не так. При использовании автоматических инструментов рефакторинга (например, ReSharper) иногда может потребоваться обратиться к аргументу метода по его имени:

Вот, как это должно быть:

8. Инициализаторы свойств (property) — C# 6

Инициализаторы свойств позволяют задавать начальные значения для свойств:

Польза их использования заключается в том, что вы не можете объявить setter, тем самым делая свойства неизменяемыми. Инициализаторы свойств хорошо работают в связке с синтаксисом первичного конструктора в C# 6.

9. Операторы as и is

Is — совместимость типов. Возвращает значение true, если вычисленный левый операнд может быть приведен к типу, указанному в правом операнде (статический тип).

As — преобразование типов. Возвращает левый операнд, приведенный к типу, заданному правым операндом (статический тип), но as возвращает null , где (T)x вызывает исключение.

10. Ключевое слово yield

Ключевое слово yield позволяет заполнить интерфейс IEnumerable объектами (items). Следующий пример вернет все степени двойки от 2 до 2 в степени 8 (то есть 2, 4, 8, 16, 32, 128, 256):

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

Оператор return

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

Возврат из функции

Функция может завершать выполнение и осуществлять возврат в вызывающую программу двумя способами. Первый способ используется тогда, когда после выполнения последнего оператора в функции встречается закрывающая фигурная скобка (>). (Конечно, это просто жаргон, ведь в настоящем объектном коде фигурной скобки нет!) Например, функция pr_reverse() в приведенной ниже программе просто выводит на экран в обратном порядке строку Мне нравится С , а затем возвращает управление вызывающей программе.

Как только строка выведена на экран, функции pr_reverse() «делать больше нечего», поэтому она возвращает управление туда, откуда она была вызвана.

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

В функции может быть несколько операторов return . Например, в следующей программе функция find_substr() возвращает начальную позицию подстроки в строке или же возвращает —1, если подстрока, наоборот, не найдена. В этой функции для упрощения кодирования используются два оператора return .

Возврат значений

Все функции, кроме тех, которые относятся к типу void , возвращают значение. Это значение указывается выражением в операторе return . Стандарт С89 допускает выполнение оператора return без указания выражения внутри функции, тип которой отличен от void . В этом случае все равно происходит возврат какого-нибудь произвольного значения. Но такое положение дел, мягко говоря, никуда не годится! Поэтому в Стандарте С99 (да и в C++) предусмотрено, что в функции, тип которой отличен от void , в операторе return необходимо обязательно указать возвращаемое значение. То есть, согласно С99, если для какой-либо функции указано, что она возвращает значение, то внутри этой функции у любого оператора return должно быть свое выражение. Однако если функция, тип которой отличен от void , выполняется до самого конца (то есть до закрывающей ее фигурной скобки), то возвращается произвольное (непредсказуемое с точки зрения разработчика программы!) значение. Хотя здесь нет синтаксической ошибки, это является серьезным упущением и таких ситуаций необходимо избегать.

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

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

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

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

Второй вид включает в себя функции, которые обрабатывают информацию и возвращают значение, которое показывает, успешно ли была выполнена эта обработка. Примером является библиотечная функция fclose() , которая закрывает файл. Если операция закрытия была завершена успешно, функция возвращает 0, а в случае ошибки она возвращает EOF .

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

Иногда функции, которые, казалось бы, фактически не выдают содержательный результат, все же возвращают какое-то значение. Например, printf() возвращает количество выведенных символов. Если бы нашлась такая программа, которая на самом деле проверяла бы это значение, то это было бы что-то необычное… Другими словами, хотя все функции, за исключением относящихся к типу void , возвращают значения, вовсе не нужно стремиться использовать эти значения во что бы то ни стало. Часто при обсуждении значений, возвращаемых функциями, возникает такой довольно распространенный вопрос: «Неужели не обязательно присваивать возвращенное значение какой-либо переменной? Не повиснет ли оно где-нибудь и не приведет ли это в дальнейшем к каким-либо неприятностям?» Отвечая на этот вопрос, повторим, что присваивание отнюдь не является обязательным, причем отсутствие его не станет причиной каких-либо неприятностей. Если возвращаемое значение не входит ни в один из операторов присваивания, то это значение будет просто отброшено. Отметим также, что такое отбрасывание значения встречается очень часто. Проанализируйте следующую программу, в которой используется функция mul() :

В строке 1 значение, возвращаемое функцией mul() , присваивается переменной z . В строке 2 возвращаемое значение не присваивается, но используется функцией printf() . И наконец, в строке 3 возвращаемое значение теряется, потому что не присваивается никакой из переменных и не используется как часть какого-либо выражения.

Возвращаемые указатели

Хотя с функциями, которые возвращают указатели, обращаются так же, как и с любыми другими функциями, все же будет полезно познакомиться с некоторыми основными понятиями и рассмотреть соответствующий пример. Указатели не являются ни целыми, ни целыми без знака. Они являются адресами в памяти и относятся к особому типу данных. Такая особенность указателей определяется тем, что арифметика указателей (адресная арифметика) работает с учетом параметров базового типа. Например, если указателю на целое придать минимальное (ненулевое) приращение, то его текущее значение станет на четыре больше, чем предыдущее (при условии, что целые значения занимают 4 байта). Вообще говоря, каждый раз, когда значение указателя увеличивается (уменьшается) на минимальную величину, то он указывает на последующий (предыдущий) элемент, имеющий базовый тип указателя. Так как размеры разных типов данных могут быть разными, то компилятор должен знать тип данных, на которые может указывать указатель. Поэтому в объявлении функции, которая возвращает указатель, тип возвращаемого указателя должен декларироваться явно. Например, нельзя объявлять возвращаемый тип как int * , если возвращается указатель типа char * ! Иногда (правда, крайне редко!) требуется, чтобы функция возвращала «универсальный» указатель, т.е. указатель, который может указывать на данные любого типа. Тогда тип результата функции следует определить как void * .

Чтобы функция могла возвратить указатель, она должна быть объявлена как возвращающая указатель на нужный тип. Например, следующая функция возвращает указатель на первое вхождение символа, присвоенного переменной с , в строку s . Если этого символа в строке нет, то возвращается указатель на символ конца строки (’0′).

Вот небольшая программа, в которой используется функция match() :

Эта программа сначала считывает строку, а затем символ. Потом проводится поиск местонахождения символа в строке. При наличии символа в строке переменная p укажет на него, и программа выведет строку, начиная с найденного символа. Если символ в строке не найден, то p укажет на символ конца строки ( ’0′ ), причем *p будет представлять логическое значение ЛОЖЬ (false). В таком случае программа выведет сообщение Символа нет .

Функция типа void

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

Вот пример использования функции print_vertical() :

И еще одно замечание: в ранних версиях С ключевое слово void не определялось. Таким образом, в программах, написанных на этих версиях С, функции, которые не возвращали значений, просто имели по умолчанию тип int — и это несмотря на то, что они не возвращали никаких значений!

Цукерберг рекомендует:  Билл Гейтс 5 книг, которые стоит прочитать летом 2020
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих