Skip to content

Latest commit

 

History

History
884 lines (619 loc) · 59.3 KB

t5_function.md

File metadata and controls

884 lines (619 loc) · 59.3 KB
Предыдущая лекция   Следующая лекция
Объявление множества. Работа с датами. Кортежи. Содержание Делегаты, события и лямбды

Общие сведения о подпрограммах. Определение и вызов подпрограмм. Область видимости и время жизни переменной. Механизм передачи параметров.

Содрано отсюда

Методы

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

Общее определение методов выглядит следующим образом:

[модификаторы] тип_возвращаемого_значения название_метода ([параметры])
{
    // тело метода
}

Модификаторы и параметры необязательны.

Например, по умолчанию консольная программа на языке C# должна содержать как минимум один метод - метод Main, который является точкой входа в приложение:

static void Main(string[] args)
{
     
}

В Rider-е по-другому. Консольная программа выглядит как скрипт, где точкой входа является первая выполняемая строка. Хотя несомненно где-то под капотом метод Main есть

Ключевое слово static является модификатором. Далее идет тип возвращаемого значения. В данном случае ключевое слово void указывает на то, что метод ничего не возвращает.

Далее идет название метода - Main и в скобках параметры - string[] args. И в фигурные скобки заключено тело метода - все действия, которые он выполняет. В данном случае метод Main пуст, он не содержит никаких операторов и по сути ничего не выполняет.

Определим еще пару методов:

// метод Main я убрал

static void SayHello()
{
    Console.WriteLine("Hello");
}
static void SayGoodbye()
{
    Console.WriteLine("GoodBye");
}

В данном случае определены два метода: SayHello и SayGoodbye. Оба метода имеют модификатор static, а в качестве возвращаемого типа для них определен тип void. То есть данные методы ничего не возвращают, просто производят некоторые действия. И также оба метода не имеют никаких параметров, поэтому после названия метода указаны пустые скобки.

Оба метода выводят на консоль некоторую строку. Причем для вывода на консоль методы используют другой метод, который определен в .NET по умолчанию - Console.WriteLine().

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

Вызов методов

Чтобы использовать методы SayHello и SayGoodbye в программе, нам надо их явно вызвать.

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

название_метода (значения_для_параметров_метода);

Например, вызовем методы SayHello и SayGoodbye:

SayHello();
SayGoodbye();

Console.ReadKey();

static void SayHello()
{
    Console.WriteLine("Hello");
}
static void SayGoodbye()
{
    Console.WriteLine("GoodBye");
}

Консольный вывод программы:

Hello
GoodBye

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

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

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

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

return возвращаемое значение;

Например, определим еще пару методов:

static string GetHello()
{
    return "Hello";
}
static int GetSum()
{
    int x = 2;
    int y = 3;
    int z = x + y;
    return z;
}

Метод GetHello имеет тип string, следовательно, он должен возвратить строку. Поэтому в теле метода используется оператор return, после которого указана возвращаемая строка.

Метод GetSum имеет тип int, следовательно, он должен возвратить значение типа int - целое число. Поэтому в теле метода используется оператор return, после которого указано возвращаемое число (в данном случае результат суммы переменных x и y).

После оператора return также можно указывать сложные выражения, которые возвращают определенный результат. Например:

static int GetSum()
{
    int x = 2;
    int y = 3;
    return x + y;
}

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

static string GetHello()
{
    Console.WriteLine("Hello");
}

Также между возвращаемым типом метода и возвращаемым значением после оператора return должно быть соответствие. Например, в следующем случае возвращаемый тип - int, но метод возвращает строку (тип string), поэтому такое определение метода некорректно:

static int GetSum()
{
    int x = 2;
    int y = 3;
    return "5"; // ошибка - надо возвращать число
}

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

string message = GetHello();
int sum = GetSum();

Console.WriteLine(message);  // Hello
Console.WriteLine(sum);     // 5

Console.ReadKey();

static string GetHello()
{
    return "Hello";
}
static int GetSum()
{
    int x = 2;
    int y = 3;
    return x + y;
}

Метод GetHello возвращает значение типа string. Поэтому мы можем присвоить это значение какой-нибудь переменной типа string: string message = GetHello();

Второй метод - GetSum - возвращает значение типа int, поэтому его можно присвоить переменной, которая принимает значение этого типа: int sum = GetSum();.

Выход из метода

Оператор return не только возвращает значение, но и производит выход из метода. Поэтому он должен определяться после остальных инструкций. Например:

static string GetHello()
{
    return "Hello";
    Console.WriteLine("After return");
}

С точки зрения синтаксиса данный метод корректен, однако его инструкция Console.WriteLine("After return") не имеет смысла - она никогда не выполнится, так как до ее выполнения оператор return возвратит значение и произведет выход из метода.

Однако мы можем использовать оператор return и в методах с типам void. В этом случае после оператора return не ставится никакого возвращаемого значения (ведь метод ничего не возвращает). Типичная ситуация - в зависимости от опеределенных условий произвести выход из метода:

static void SayHello()
{
    int hour = 23;
    if(hour > 22)
    {
        return;
    }
    else
    {
        Console.WriteLine("Hello");
    }
}

Сокращенная запись методов

Если метод в качестве тела определяет только одну инструкцию, то мы можем сократить определение метода. Например, допустим у нас есть метод:

static void SayHello()
{
    Console.WriteLine("Hello");
}

Мы можемего сократить следующим образом:

static void SayHello() => Console.WriteLine("Hello");

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

Подобным образом мы можем сокращать методы, которые возвращают значение:

static string GetHello()
{
    return "hello";
}

Анлогичен следующему методу:

static string GetHello() => "hello";

Параметры методов

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

static int Sum(int x, int y)
{
    return x + y;
}

Метод Sum имеет два параметра: x и y. Оба параметра представляют тип int. Поэтому при вызове данного метода нам обязательно надо передать на место этих параметров два числа.

int result = Sum(10, 15);
Console.WriteLine(result);  // 25
    
Console.ReadKey();

static int Sum(int x, int y)
{
    return x + y;
}

При вызове метода Sum значения передаются параметрам по позиции. Например, в вызове Sum(10, 15) число 10 передается параметру x, а число 15 - параметру y. Значения, которые передаются параметрам, еще называются аргументами. То есть передаваемые числа 10 и 15 в данном случае являются аргументами.

Иногда можно встретить такие определения как формальные параметры и фактические параметры. Формальные параметры - это собственно параметры метода (в данном случае x и y), а фактические параметры - значения, которые передаются формальным параметрам. То есть фактические параметры - это и есть аргументы метода.

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

int a = 25;
int b = 35;
int result = Sum(a, b);
Console.WriteLine(result);  // 60

result = Sum(b, 45);
Console.WriteLine(result);  // 80

// "a + b + 12" представляет значение параметра x
result = Sum(a + b + 12, 18);

Console.WriteLine(result);  // 90

Console.ReadKey();

static int Sum(int x, int y)
{
    return x + y;
}

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

int a;
int b = 9;

// Ошибка - переменной a не присвоено значение
Sum(a, b);

Console.ReadKey();

static int Sum(int x, int y)
{
    return x + y;
}

При передаче значений параметрам важно учитывать тип параметров: между аргументами и параметрами должно быть соответствие по типу. Например:

// Name: Tom  Age: 24
Display("Tom", 24); 

Console.ReadKey();

static void Display(string name, int age)
{
    Console.WriteLine($"Name: {name}  Age: {age}");
}

В данном случае первый параметр метода Display представляет тип string, поэтому мы должны передать этому параметру значение типа string, то есть строку. Второй параметр представляет тип int, поэтому должны передать ему целое число, которое соответствует типу int.

Другие данные параметрам мы передать не можем. Например, следующий вызов метода Display будет ошибочным:

// Ошибка! несоответствие значений типам параметров
Display(45, "Bob"); 

Необязательные параметры

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

static int OptionalParam(
    int x, int y, int z=5, int s=4)
{
    return x + y + z + s;
}

Так как последние два параметра объявлены как необязательные, то мы можем один из них или оба опустить:

OptionalParam(2, 3);

OptionalParam(2, 3, 10);

Console.ReadKey();

Именованные параметры

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

static int OptionalParam(int x, int y, int z=5, int s=4)
{
    return x + y + z + s;
}

OptionalParam(x:2, y:3);
    
//Необязательный параметр z использует значение по умолчанию
OptionalParam(y:2, x:3, s:10);

Console.ReadKey();

Передача параметров по ссылке и значению. Выходные параметры

Существует два способа передачи параметров в метод в языке C#: по значению и по ссылке.

Передача параметров по значению

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

// параметры передаются по значению
Sum(10, 15);

Console.ReadKey();

static int Sum(int x, int y)
{
    return x + y;
}

Передача параметров по ссылке и модификатор ref

При передаче параметров по ссылке перед параметрами используется модификатор ref:

int x = 10;
int y = 15;
Addition(ref x, y); // вызов метода
Console.WriteLine(x);   // 25

Console.ReadLine();

// параметр x передается по ссылке
static void Addition(ref int x, int y)
{
    x += y;
}

Обратите внимание, что модификатор ref указывается, как при объявлении метода, так и при его вызове в методе Main.

Сравнение передачи по значению и по ссылке

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

Рассмотрим два аналогичных примера. Первый пример - передача параметра по значению:

int a = 5;
Console.WriteLine(
    $"Начальное значение переменной a = {a}");

// Передача переменных по значению
// После выполнения этого кода по-прежнему a = 5, 
// так как мы передали лишь ее копию
IncrementVal(a);

Console.WriteLine(
    $"Переменная a после передачи по значению равна = {a}");

Console.ReadKey();

// передача по значению
static void IncrementVal(int x)
{
    x++;
    Console.WriteLine($"IncrementVal: {x}");
}

Консольный вывод:

Начальное значение переменной a = 5
IncrementVal: 6
Переменная a после передачи по значению равна = 5

При вызове метод IncrementVal получает копию переменной a и увеличивает значение этой копии. Поэтому в самом методе IncrementVal мы видим, что значение параметра x увеличилось на 1, но после выполнения метода переменная a имеет прежнее значение - 5. То есть изменяется копия, а сама переменная не изменяется.

Второй пример - аналогичный метод с передачей параметра по ссылке:

int a = 5;

Console.WriteLine(
    $"Начальное значение переменной a  = {a}");

// Передача переменных по ссылке
// После выполнения этого кода a = 6, 
// так как мы передали саму переменную
IncrementRef(ref a);

Console.WriteLine(
    $"Переменная a после передачи ссылке равна = {a}");
    
Console.ReadKey();

// передача по ссылке
static void IncrementRef(ref int x)
{
    x++;
    Console.WriteLine($"IncrementRef: {x}");
}

Консольный вывод:

Начальное значение переменной a = 5
IncrementRef: 6
Переменная a после передачи по ссылке равна = 6

В метод IncrementRef передается ссылка на саму переменную a в памяти. И если значение параметра в IncrementRef изменяется, то это приводит и к изменению переменной a, так как и параметр и переменная указывают на один и тот же адрес в памяти.

Выходные параметры. Модификатор out

Выше мы использовали входные параметры. Но параметры могут быть также выходными. Чтобы сделать параметр выходным, перед ним ставится модификатор out:

static void Sum(int x, int y, out int a)
{
    a = x + y;
}

Здесь результат возвращается не через оператор return, а через выходной параметр. Использование в программе:

int x = 10;
    
int z;
    
Sum(x, 15, out z);
    
Console.WriteLine(z);

Console.ReadKey();

Причем, как и в случае с ref ключевое слово out используется как при определении метода, так и при его вызове.

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

static void Sum(int x, int y, out int a)
{
    Console.WriteLine(x+y);
}

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

int x = 10;
int area;
int perimetr;
GetData(x, 15, out area, out perimetr);
Console.WriteLine("Площадь : " + area);
Console.WriteLine("Периметр : " + perimetr);

Console.ReadKey();

static void GetData(
    int x, int y, out int area, out int perim)
{
    area= x * y;
    perim= (x + y)*2; 
}

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

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

С появлением кортежей особого смысла в out параметрах нет, проще вернуть сколько угодно результатов завернув их в кортеж:

static (int area, int perim) GetData(int x, int y) {
  return (x*x, (x + y)*2)    
} 

Массив параметров и ключевое слово params

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

static void Addition(params int[] integers)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += integers[i];
    }
    Console.WriteLine(result);
}
 
Addition(1, 2, 3, 4, 5);
    
int[] array = new int[] { 1, 2, 3, 4 };
Addition(array);

Addition();
Console.ReadLine();

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

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

//Так работает
static void Addition( 
    int x, string mes, params int[] integers)
{

}

Вызов подобного метода:

Addition(2, "hello", 1, 3, 4);

Однако после параметра с модификатором params мы НЕ можем указывать другие параметры. То есть следующее определение метода недопустимо:

//Так НЕ работает
static void Addition(
    params int[] integers, int x, string mes)
{

}

Массив в качестве параметра

Также этот способ передачи параметров надо отличать от передачи массива в качестве параметра:

// передача параметра с params
static void Addition(params int[] integers)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += integers[i];
    }
    Console.WriteLine(result);
}

// передача массива
static void AdditionMas(int[] integers, int k)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += (integers[i]*k);
    }
    Console.WriteLine(result);
}
 
Addition(1, 2, 3, 4, 5);

int[] array = new int[] { 1, 2, 3, 4 };
AdditionMas(array, 2);

Console.ReadLine();

Так как метод AdditionMas принимает в качестве параметра массив без ключевого слова params, то при его вызове нам обязательно надо передать в качестве параметра массив.

Область видимости (контекст) переменных

Каждая переменная доступна в рамках определенного контекста или области видимости. Вне этого контекста переменная уже не существует.

Существуют различные контексты:

Контекст класса. Переменные, определенные на уровне класса, доступны в любом методе этого класса

Контекст метода. Переменные, определенные на уровне метода, являются локальными и доступны только в рамках данного метода. В других методах они недоступны

Контекст блока кода. Переменные, определенные на уровне блока кода, также являются локальными и доступны только в рамках данного блока. Вне своего блока кода они не доступны.

Например, пусть класс Program определен следующим образом:

class Program // начало контекста класса
{
    static int a = 9; // переменная уровня класса
     
    static void Main(string[] args) // начало контекста метода Main
    {
        int b = a - 1; // переменная уровня метода
 
        { // начало контекста блока кода
             
            int c = b - 1; // переменная уровня блока кода
 
        }  // конец контекста блока кода, переменная с уничтожается
 
        //так нельзя, переменная c определена в блоке кода
        //Console.WriteLine(c);
 
        //так нельзя, переменная d определена в другом методе
        //Console.WriteLine(d);
 
        Console.Read();
 
    } // конец контекста метода Main, переменная b уничтожается
 
    void Display() // начало контекста метода Display
    {
        // переменная a определена в контексте класса, поэтому доступна
        int d = a + 1;
 
    } // конец конекста метода Display, переменная d уничтожается
 
} // конец контекста класса, переменная a уничтожается

Здесь определенно четыре переменных: a, b, c, d. Каждая из них существует в своем контексте. Переменная a существует в контексте всего класса Program и доступна в любом месте и блоке кода в методах Main и Display.

Переменная b существует только в рамках метода Main. Также как и переменная d существует в рамках метода Display. В методе Main мы не можем обратиться к переменной d, так как она в другом контексте.

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

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

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

class Program
{
    static int a = 9; // переменная уровня класса
     
    static void Main(string[] args)
    {
        int a = 5; // скрывает переменную a, которая объявлена на уровне класса
        Console.WriteLine(a); // 5
    }
}

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

Секреты хорошей функции (копипаст с хабра)

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

  • Она внятно названа
  • Соответствует принципу единственной обязанности
  • Содержит xml-комментарий
  • Возвращает значение
  • Состоит не более чем из 50 строк
  • Она идемпотентная и, если это возможно, чистая

Единственная ответственность

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

Здесь лучше привести пример. Вот функция, делающая более одной «вещи»:

def calculate_and print_stats(list_of_numbers):
    sum = sum(list_of_numbers)
    mean = statistics.mean(list_of_numbers)
    median = statistics.median(list_of_numbers)
    mode = statistics.mode(list_of_numbers)

    print('-----------------Stats-----------------')
    print('SUM: {}'.format(sum)
    print('MEAN: {}'.format(mean)
    print('MEDIAN: {}'.format(median)
    print('MODE: {}'.format(mode)

А именно две: вычисляет набор статистических данных о списке чисел и выводит их в STDOUT. Функция нарушает правило: должна быть единственная конкретная причина, по которой ее, возможно, потребовалось бы изменить. В данном случае просматриваются две очевидные причины, по которым это понадобится: либо потребуется вычислять новую или иную статистику, либо потребуется изменить формат вывода. Поэтому данную функцию лучше переписать в виде двух отдельных функций: одна будет выполнять вычисления и возвращать их результаты, а другая – принимать эти результаты и выводить их в консоль. Функцию (вернее, наличие у нее двух обязанностей) с потрохами выдает слово and в ее названии.

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

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

XML-комментарии

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

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

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

Функции можно (и следует) трактовать как маленькие самодостаточные программы. Они принимают некоторый ввод в форме параметров и возвращают результат. Параметры, конечно, опциональны. А вот возвращаемые значения обязательны с точки зрения внутреннего устройства. Если вы даже попытаетесь написать функцию, которая не возвращает значения – не сможете. Если функция даже не станет возвращать значения, то по-умолчанию вернётся void.

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

with open('foo.txt', 'r') as input_file:
    for line in input_file:
        if line.strip().lower().endswith('cat'):
            # ... делаем с этими строками что-нибудь полезное

Строка if line.strip().lower().endswith('cat'): работает, поскольку каждый из строковых методов (strip(), lower(), endswith()) в результате вызова функции возвращает строку.

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

«Она всего лишь [какая-то операция, связанная с вводом/выводом, например, сохранение значения в базе данных]. Здесь я не могу вернуть ничего полезного.»

Не соглашусь. Функция может вернуть True, если операция завершилась успешно.

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

Здесь – два замечания. Во-первых, всеми силами старайтесь так не делать. Во-вторых, снабжать функцию каким-либо аргументом лишь для того, чтобы узнать, что она изменилась – в лучшем случае удивительно, а в худшем – попросту опасно. Вместо этого, как и при работе со строковыми методами, старайтесь возвращать новый экземпляр параметра, в котором уже отражены примененные к нему изменения. Даже если это не получается делать, поскольку создание копии какого-то параметра сопряжено с чрезмерными издержками, все равно можно откатываться к предложенному выше варианту «Вернуть True, если операция завершилась успешно».

«Мне нужно возвращать несколько значений. Нет такого единственного значения,которое в данном случае было бы целесообразно возвращать.»

Этот аргумент немного надуманный, но мне доводилось его слышать. Ответ, разумеется, как раз в том, что автор и хотел сделать – но не знал как: для возврата нескольких значений используйте кортеж.

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

Длина функции

Я не раз признавался, что довольно туп. Могу одновременно держать в голове примерно три вещи. Если вы дадите мне прочесть 200-строчную функцию и спросите, что она делает, я, вероятно, буду таращиться на нее не менее 10 секунд. Длина функции прямо сказывается на ее удобочитаемости и, следовательно, на поддержке. Поэтому старайтесь, чтобы ваши функции оставались короткими. 50 строк – величина, взятая совершенно с потолка, но мне она кажется разумной. (Надеюсь), что большинство функций, которые вам доведется писать, будут значительно короче.

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

Итак, что же делать, если ваша функция получилась слишком длинной? РЕФАКТОРИТЬ! Вероятно, вам приходится заниматься рефакторингом постоянно, даже если вы не знаете этого термина. Рефакторинг – это попросту изменение структуры программы, без изменения ее поведения. Поэтому, извлечение нескольких строк кода из длинной функции и превращение их в самостоятельную функцию – это один из типов рефакторинга. Оказывается, это еще и наиболее распространенный, и самый быстрый способ продуктивного укорачивания длинных функций. Поскольку вы даете этим новым функциям подходящие имена, получающийся у вас код гораздо проще читать. Я написал целую книгу о рефакторинге (на самом деле, я им постоянно занимаюсь), так что здесь вдаваться в детали не буду. Просто знайте, что, если у вас есть слишком длинная функция – то ее следует рефакторить.

Идемпотентность и функциональная чистота

Заголовок этого раздела может показаться слегка устрашающим, но концептуально раздел прост. Идемпотентная функция при одинаковом наборе аргументов всегда возвращает одно и то же значение, независимо от того, сколько раз ее вызывают. Результат не зависит от нелокальных переменных, изменяемости аргументов или от любых данных, поступающих из потоков ввода/вывода. Следующая функция add_three(number) идемпотентна:

def add_three(number):
    """вернуть *число* + 3."""
    return number + 3

Независимо от того, сколько раз мы вызовем add_three(7), ответ всегда будет равен 10. А вот другой случай – функция, не являющаяся идемпотентной:

def add_three():
    """Вернуть 3 + число, введенное пользователем."""
    number = int(input('Enter a number: '))
    return number + 3

Эта откровенно надуманная функция не идемпотентна, поскольку возвращаемое значение функции зависит от ввода/вывода, а именно – от числа, введенного пользователем. Разумеется, при разных вызовах add_three() возвращаемые значения будут отличаться. Если мы дважды вызовем эту функцию, то пользователь в первом случае может ввести 3, а во втором – 7, и тогда два вызова add_three() вернут 6 и 10 соответственно.

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

Почему идемпотентность так важна

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

Что такое «чистая» функция?

В функциональном программировании функция считается чистой, если она, во-первых, идемпотентна, а во-вторых – не вызывает наблюдаемых побочных эффектов. Не забывайте: функция идемпотентна, если всегда возвращает один и тот же результат при конкретном наборе аргументов. Однако, это не означает, что функция не может влиять на другие компоненты – например, на нелокальные переменные или потоки ввода/вывода. Например, если бы идемпотентная версия вышеприведенной функции add_three(number) выводила результат в консоль, а лишь затем возвращала бы его, она все равно считалась бы идемпотентной, поскольку при ее обращении к потоку ввода/вывода эта операция доступа никак не влияет на значение, возвращаемое от функции. Вызов print() – это просто побочный эффект: взаимодействие с остальной программой или системой как таковой, происходящее наряду с возвратом значения.

Давайте немного разовьем наш пример с add_three(number). Можно написать следующий код, чтобы определить, сколько раз была вызвана add_three(number):

add_three_calls = 0

def add_three(number):
    """Вернуть *число* + 3."""
    global add_three_calls
    print(f'Returning {number + 3}')
    add_three_calls += 1
    return number + 3

def num_calls():
    """Вернуть, сколько раз была вызвана *add_three*."""
    return add_three_calls

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

Чистая функция не оказывает побочных эффектов. Она не только не использует никаких «внешних данных» при расчете значения, но и не взаимодействует с остальной программой/системой, только вычисляет и возвращает указанное значение. Следовательно, хотя наше новое определение add_three(number) остается идемпотентным, эта функция уже не чистая.

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

Короче говоря, они не оказывают «жуткого дальнодействия», выражаясь словами Эйнштейна (но в контексте информатики, а не физики). Они не изменяют каким-либо образом остальные части программы или системы. В императивном программировании (а именно им вы и занимаетесь, когда пишете код на Python),такие функции – самые безопасные. Они известны своей тестируемостью и удобством в поддержке; более того, поскольку они идемпотентны, тестирование таких функций гарантированно будет столь же быстрым, как и выполнение. Сами тесты также просты: не приходится подключаться к базе данных либо имитировать какие-либо внешние ресурсы, готовить стартовую конфигурацию кода, а по окончании работы не нужно ничего подчищать.

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

Предыдущая лекция   Следующая лекция
Объявление множества. Работа с датами. Кортежи. Содержание Делегаты, события и лямбды