×
Вы действительно хотите открыть доступ к тестированию по курсу C# 5.0 Стартовый на 40 дней?
ВИДЕОУРОК №8. Методы и рекурсия в C#
Здравствуйте! Тема нашего сегодняшнего урока – Варианты использования методов. На этом занятии мы с вами научимся понимать более глубоко понимать работу методов. Более глубоко понимать работу рекурсии. Рекурсия – это такой специальный механизм, который используется при работе с методами мы научимся понимать перегрузку методов. И также будем использовать методы с опциональными именованными параметрами. Также мы рассмотрим еще несколько интересных примеров, которые позволят нам закрепить понимание методов и научится работать с методами. А мы вами перейдем к презентации, к первому слайду и посмотрим что же такое перегрузка методов. Обратите внимание, вот здесь представлены программные коды. Мы создаем метод с именем Method, который ничего не принимает и ничего не возвращает. В теле метода выводим на экран строку Hello. Ниже мы создаем, обратите внимание, мы создаем одноименный метод, который уже у нас есть. Но чем отличаются эти методы? Количеством параметров. Обратите внимание, у нас имеется два метода с одинаковым именем но с различным количеством или типом параметров. Ниже мы создаем еще один метод с именем Method, который принимает один целочисленный параметр. И получается у нас в нашей программе может иметься три, или больше одноименных метода, и эти методы должны отличатся количеством, либо типом параметров. Давайте перейдем к программному коду и посмотрим как это выглядит в коде. Мы заходим в первый пример.
1: using System; 2: 3: // Перегрузка методов. 4: 5: namespace Methods 6: { 7: class Program 8: { 9: // Перегруженные методы могут отличаться типом и количеством аргументов, а также ref и out параметрами. 10: 11: static void Function() // 1-я перегрузка. 12: { 13: Console.WriteLine("Hello!"); 14: } 15: 16: static void Function(string s) // 2-я перегрузка. 17: { 18: Console.WriteLine(s); 19: } 20: 21: static void Function(int i) // 3-я перегрузка. 22: { 23: Console.WriteLine(i); 24: } 25: 26: static void Function(double d) // 4-я перегрузка. 27: { 28: Console.WriteLine(d); 29: } 30: 31: static void Function(string s, int i) // 5-я перегрузка. 32: { 33: Console.WriteLine(s + i); 34: } 35: 36: static void Function(int i, string s) // 6-я перегрузка. 37: { 38: Console.WriteLine(i + s); 39: } 40: 41: static void Function(ref int i, string s) // 7-я перегрузка. 42: { 43: Console.WriteLine(i + s); 44: } 45: 46: // Перегруженные методы не могут отличаться возвращаемыми значениями! 47: //static string Function(string s) // 8-я перегрузка. 48: //{ 49: // return s; 50: //} 51: 52: // Перегруженные методы не могут отличаться друг от друга только параметрами ref и out! 53: //static void Function(out int i, string s) 54: //{ 55: // i = 5; 56: // Console.WriteLine(i + s); // 9-я перегрузка. 57: //} 58: 59: static void Main() 60: { 61: Function(); // 1-я перегрузка. 62: Function("A"); // 2-я перегрузка. 63: Function(1); // 3-я перегрузка. 64: Function(3.14); // 4-я перегрузка. 65: Function("B ", 2); // 5-я перегрузка. 66: Function(3, " C"); // 6-я перегрузка. 67: 68: int variable = 5; 69: Function(ref variable, "D"); // 7-я перегрузка. 70: 71: // Delay. 72: Console.ReadKey(); 73: } 74: } 75: }
На 11 строке мы создаем метод с именем Function, который ничего не принимает и ничего не возвращает. Выводит на экран строку. Ниже на 16 строке мы создаем одноименный метод Function, который принимает строковой аргумент и ничего не возвращает. Ниже на 21 строке мы строке мы создаем одноименный метод Function, который принимает один целочисленный аргумент и ничего не возвращает. Ниже на 26 строке мы создаем еще один одноименный метод с именем Function, который принимает Double аргумент и ничего не возвращает. Смотрите, ниже на 31 строке мы создаем еще один одноименный методы Function, который принимает два аргумента: первый строковой, второй – целочисленный. На 36 строке мы создаем еще один метод Function, который принимает два параметра, но обратите внимание, у нас уже имеется метод с такими параметрами. Но очередность этих параметров отличается. Если отличается очередность, то мы можем сказать, что по сути это разные методы. Смотрим ниже, у нас имеется на 41 строке метод с именем Function, который первый параметр принимает как ref параметр, обратите внимание, потому что на 36 строке у нас уже имеется метод с однотипными параметрами, которые идут в такой же очередности, как и на 41 строке. Но тем они отличаются. Тем что на 36 строке – это обычный параметр in – входной параметр. А на 41 строке – это у нас ref – выходной параметр. Помните мы говорили, что программист будь осторожен, если ты передашь методу в качестве первого параметра какую-то свою переменную, то возможно ее значение может изменится. Обратите внимание, еще раз мы видим что у нас имеется набор одноимённых методов. Но эти методы отличаются чем? Они отличаются типом и количеством аргументов. Типом и количеством параметров. И даже если у нас тип и количество параметров совпадает, и тогда мы что будем учитывать? Очередность этих параметров. Также можно учитывать входной ли это параметр. In это чисто входной параметр, а ref – выходной. Давайте посмотрим, еще несколько правил. Перегружаемые методы не могут отличатся возвращаемыми значениями. Почему? Потому что если вы представите себя компилятором то вы можете отличить перегрузки этих методов? Ну конечно, я их буду отличать по типу и количеству, по порядку аргументов. Но мне сложно их будет различить по типу возвращаемого значения. Я скажу сложно – но можно. Есть языки в которых перегрузки не отличаются друг от друга по типу возвращаемых значений по типу возвращаемых значений. Не отличаются. Таким языком является C#. Есть языки в которых перегрузки отличаются, и есть такой близки й к нам язык, который называется MSIL – Microsoft Intermediate Language. Его еще называют CIL – Command Intermediate Language. Это низкоуровневый язык, для работы с платформой .NET. Низкоуровневый язык, который содержит в себе удобочитаемые мнемоники для кодов виртуальной машины. Потому что виртуальная машина она принимает на вход коды, которые наш компилятор, наша программа преобразуется в эти коды, и потом эти коды принимает виртуальная машина и интерпретирует их. Но мы не будем об этом говорить. Если вам интересно вы можете немного почитать об этом языке, немного узнать. Мы на этом курсе его разбирать не будем, а уже наследующих курсах мы сделаем небольшой акцент и даже посмотрим несколько примеров с использованием этого промежуточного языка MSIL. Хорошо. Значит мы видим, что методы, которые встречаются у нас в программе, причем эти методы имеют одинаковое имя, то есть одинаковый идентификатор, то такие методы принято называть перегрузками. Вот мы и их называем: первая перегрузка метода, вторая перегрузка метода, третья и т.д. Правило следующее. В языке C# перегрузки методов не могут отличаться возвращаемыми значениями. Обратите внимание, Вы помните когда мы с вами говорили о сигнатуре метода? Что начиная с третьей спецификации Microsoft в понятие сигнатуры не включает возвращаемое значение. Сигнатура – это имя метода и его параметры. Возвращаемое значение оно само по себе. Почему так сделал Microsoft? Потому что общие спецификации, другие спецификации, не только языка C# тоже пришли к такому стандарту. Поэтому есть OMG консорциум. Object Managment Group. Который создает общие спецификации для языков программирования. Вы можете зайти на сайт omg.org и там вы увидите много спецификаций, спецификаций по ООП, по другим языкам и почитайте их, иногда это достаточно интересно. И поэтому так как все спецификации сказали, что давайте мы не будем в понятие сигнатуры включать возвращаемое значение, потому что когда я программист. Я говорю, чем отличаются друг от друга перегруженные методы? Сигнатурами. А если возвращаемое значение будет входить в сигнатуру, то это может ввести в такое небольшое заблуждение, мы будем думать перегружаемые методы отличаются возвращаемыми значениями. Но для того чтобы организовать в таких языках как MSIL перегрузку методов по возвращаемым значениям, пришлось создавать достаточно сложный механизм, который определял бы метод, определял конкретную перегрузку по типу возвращаемого значения. Это как можно определить? Я могу это определить только из контекста использования этого метода. Если я создаю переменную int а, а здесь string b, то только в таком случае нужно выходить за пределы понимания нашего методы, за пределы использования. И смотреть где же он используется. Этот как у людей бывает. Если я нахожусь в больнице, то кто я? Я пациент. Если я нахожусь в аудитории в качестве слушателя, кто я? Студент. Если я нахожусь, например, в автомобиле, то я водитель. Если я нахожусь, например, в поезде, то кто я? Пассажир. Мы пытаемся определить по контексту. Так вот в C# такой возможности нет. Потому что такая возможность вообще в .NET присутствует, но в C# ее закрыли для того чтобы не провоцировать возникновение сложных и ошибочных ситуаций. На самом деле платформа .NET имеет намного больше возможностей чем имеется в C#. Но они закрыты специально чтобы избежать появлению ошибок в программных кодах и сделать разработку проще. Разработка выглядит немножко ограниченной, программист чувствует себя в рамках, но тем не менее мы должны помнить, что мы – Microsoft программисты, мы чаще всего разрабатываем софт для бизнес решений. А бизнес не любит, когда с ним играют. Потому что, как сказал адвокат Терразини в фильме Спрут. «Когда начинают играть деньги вся другая музыка замолкает.» Поэтому бизнес не хочет чтобы мы игрались с его деньгами и вот экспериментировали. Поэтому нам дается четкий набор инструкций из которых мы строим свои программы. С одной стороны это ограничение кого-то смущает, а с другой стороны позволяет строить более правильно и более быстро программные решения. Хорошо. Смотрим дальше. 52 строка – перегруженные методы не могут отличатся друг от друга параметрами ref и out. Если мы попытаемся создать такой метод как этот. Вот идет попытка его создания. Давайте снимем комментарий мы получим ошибку. Cannot define overloaded method 'Function' because it differs from another method only on ref and out. То есть ref от out практически ничем не отличается. Поэтому система не расценивает две таких конструкции как перегрузки методов. Теперь давайте посмотрим на вызовы этих методов у нас имеется 7 полноценных перегрузок, мы посмотрели как эти перегрузки организуются. Теперь давайте попробуем вызвать эти методы. Обратите внимание, вот у нас 61 строка. Вызывается метод с 11 строки. 62 строка здесь у нас имеется один строковой аргумент и мы можем после Function поставить круглую скобку, у нас появляется вот такая подсказочка и мы можем здесь теперь листать имеющиеся перегрузки методов. Хорошо. И еще один важный момент. А теперь давайте подумаем а почему в ООП функции и процедуры начали называть методами. Не только C# почему вообще функции и процедуры принято называть методами. Смотрите, мы с вами уже говорили, что имя метода должно четко отражать именно вид деятельности этого метода. Представьте у меня на столе лежит карандаш. И вы мне говорите: «Александр, возьми карандаш!» Смотрите, я его беру правой рукой. Я его взял. Я положил его. Вы еще раз говорите: «Александр, возьми карандаш.» Я его беру левой рукой. Положил. Вы мне говорите еще раз: «Александр, возьми карандаш!» Я его беру одновременно двумя руками. Что я делаю? Что я выполняю? Я выполняю операцию по поднятию карандаша разными методами. Я выполнил некую функциональность. Но функциональность – это звучит слишком низкоуровнево. Здесь уже вы учитываете все мои мышцы, пальцы, как они работают, как они сгибались, как зажимали этот карандаш. Смотрите, я трижды поднял карандаш. Поднял карандаш разными методами. Понимаете почему такие конструкции языков как функции и процедуры собирательно принято называть методами. Метод выполнения определенной операции. Вы скажете: «Александр, напомни мне пожалуйста, операцию поднятия карандаша ты какими методами?» Первая перегрузка – правой рукой, вторая перегрузка – левой рукой, третья перегрузка двумя руками. Ну я мог там если снять ботинки, можно было бы пальцами ноги… Но я уже не буду так экспериментировать. Теперь мы с вами понимаем, что слово Метод – это как способ выполнения определенной операции. Сложно? Нет, это не сложно. Еще раз смотрим с вами на перегрузки. Давайте еще раз зайдем в презентацию, и посмотрим, что здесь подразумевается под перегрузкой. Под перегрузкой методов понимается создание нескольких методов (два или более) с одинаковым именем, но различным типом и/или количеством параметров. Наборы параметров перегруженных методов могут отличатся порядком следования, количеством, типом. Таким образом перегрузка необходима для того, чтобы избежать создания нескольких разноименных методов, выполняющих сходные действия. Помните я карандаш поднимал. Представьте, если бы у меня не было перегрузки, чтобы я сделал первый раз? Поднял карандаш. Второй – поднял карандаш. Третий – поднял карандаш. А так мне пришлось бы говорить что это поднимание карандаша левой рукой, поднимание карандаша правой рукой. Это звучит не очень хорошо. Я выполнил операцию по поднятию карандаша. Просто я выполнял операцию поднятия карандаша тремя разными методами. Хорошо.
А мы с вами переходим к следующей секции нашего урока и рассмотрим какие же у нас бывают разновидности аргументов. Или как мы их называем – параметров. Обратите внимание, у нас бывают позиционные параметры, именованные и опциональные. Позиционные – при вызове метода, аргументы передаются в соответствии с заданной позицией и их типом. Вот у нас имеется метод у него имеется два параметра. А вот здесь мы этот метод вызываем. Обратите внимание, в соответствии параметрам в списке параметров мы и передаете параметры. Потому что если я попытаюсь 5 и Hello поменять местами, то что получится? Ошибка. Именованные параметры. Мы видим, что например при вызове метода, аргументы передаются в соответствии с их именем и типом, при этом позиция не имеет значения. Но количество аргументов должно совпадать. Обратите внимание, если мы не хотим указывать параметры в нужной позиции, такое редко встречается, но возможность такая есть. У нас имеется такой же самый метод но я хочу чтобы сначала Hello, а потом 5 передать. Здесь мне предлагается использовать такой синтаксис. Укажи сначала имя аргумента, поставь две точки и напиши значение, которое ты хочешь передать этому параметру. И поэтому я здесь меняю местами. B уже идет первым параметром, я здесь явно указываю что я хочу сюда передать. А идет вторым параметром, хотя в настоящем методе при создании он был сделан первым. Понятно, если я хочу поменять параметры местами, я использую технику именованных параметров. И еще один вид параметров – это опциональные параметры. Обратите внимание, когда мы создаем метод, мы здесь можем что сделать? Мы можем взять и аргументам присвоить некие значения по умолчанию. При вызове метода, аргументам можно передавать значения в соответствии с их позицией, типом и именем, при этом, те аргументы, которым не передано какое-то значение, уже имеют значение по умолчанию. Что это значит? Что если я а здесь присвою единицу. То если этот параметр не передать, то а будет равна1 по по умолчанию. Теперь смотрим, здесь при вызове метода мы можем использовать совместно с опциональными параметрами и именованные, и обычный позиционный подход. Поэтому смотрите, если вот здесь мы вызываем метод и используем именованный параметр b. То а будет равна1. Смотрим дальше, вызываем метод. Обратите внимание, первый параметр у нас какой? 5. Значит выведем 5. И будет конкатенация с ОК. Потому что b по умолчанию равен ОК. А при третьем вызове что будет? А = 1, B = OK. Поэтому обратите внимание, подход с опциональными параметрами мы можем даже не указывать параметры, потому что у нас имеются некие значения по умолчанию, присвоенные этим параметрам. Три разновидности параметров: Позиционные, мы с ними уже работали – это строгий подход. Именованные – это когда мы хотим от строгого подхода и просто при вызове метода указывать имя параметра, которому мы хотим присвоить какое-то значение. И вот опциональные параметры, когда при создании метода, обратите внимание, при создании метода мы присваиваем аргументам некие значения по умолчанию позволяя пользователю в дальнейшем не указывать, просто не передавать эти аргументы при вызове метода. Просто? Просто. Давайте посмотрим, как выглядит в коде работа с такими разновидностями параметров.
1: using System; 2: 3: // Именованные аргументы методов 4: 5: namespace Methods 6: { 7: class Program 8: { 9: static int Difference(int height, int width) 10: { 11: return height - width; 12: } 13: 14: static void Main() 15: { 16: // Можно вызвать метод так: 17: int difference = Difference(6, 5); 18: 19: //difference = Difference(5, 6); 20: 21: Console.WriteLine("Разность равна: {0}", difference); 22: 23: // А теперь, вот так: 24: difference = Difference(width: 5, height: 6); 25: 26: Console.WriteLine("Разность равна: {0}", difference); 27: 28: Console.ReadKey(); 29: } 30: } 31: }
Смотрим на именованные параметры методов. На 9 строке мы создаем метод с именем Difference, которые принимает два аргумента height и width. Возвращает целочисленное значение и в теле метода мы возвращаем разность аргументов. На 17 строке, обратите внимание, мы переменной difference присваиваем возвращаемое значение метода Difference. Мы передаем 6 и 5. Получается, что 6 попадает в height, а 5 в width. 6-5=1. Difference равна 1. Смотрим дальше, 21 строка, если мы поменяем местами, то у нас будет -1. Теперь смотрите на 24 строке мы вызываем метод Difference, но теперь явно указываем имя того параметра, которому мы хотим присвоить некоторое значение. И мы видим что в width мы передаем 5, хотя при создании метода он был поставлен вторым, но здесь мы его используем первым. Height, он был при создании поставлен первым, при вызове мы используем его вторым. Сказать, что это часто используемая техника – нельзя так сказать. Эта техника не часто используется, я б ы сказал что это уточняющая техника. Ну иногда параметры созданы в определенной последовательности, а когда мы читаем код, используем этот метод, наша логика подсказывает нам, что лучше можно было бы фразу построить вот так. Различие между тем-то и тем-то. Иногда хочется оттенить, чтобы другим программистам показать, что смотри ты задаешь в Difference именно width и height. Просто удобство, просто возможность. Нельзя сказать что это какая-то производственная необходимость, без которой нельзя прожить. Смотрим дальше, работа с перегрузками методов.
1: using System; 2: 3: // Перегрузка методов. 4: 5: namespace Methods 6: { 7: class Program 8: { 9: static void Operation() // 1-я перегрузка. 10: { 11: Operation("val", 10, 12.2); 12: } 13: 14: static void Operation(string value1) // 2-я перегрузка. 15: { 16: Operation(value1, 10, 12.2); 17: } 18: 19: static void Operation(string value1, int value2) // 3-я перегрузка. 20: { 21: Operation(value1, value2, 12.2); 22: } 23: 24: static void Operation(string value1, int value2, double value3) // 4-я перегрузка. 25: { 26: Console.WriteLine("{0},{1},{2}", value1, value2, value3); 27: } 28: 29: static void Main() 30: { 31: Operation(); // 1-я перегрузка. 32: Operation("val"); // 2-я перегрузка. 33: Operation("val", 10); // 3-я перегрузка. 34: Operation("val", 10, 12.2); // 4-я перегрузка. 35: 36: // Delay. 37: Console.ReadKey(); 38: } 39: } 40: }
Обратите внимание, на 9 строке здесь идет работа с перегрузками методов и одновременно с использованием именованных параметров. Смотрите, какие здесь могут возникнуть трудности. У нас имеются 4 перегрузки методов. Operation, operation с одним аргументом, operation с двумя аргументами и operation с двумя аргументами. В первом что у нас здесь происходит? На 11 строке в теле метода Operation я вызываю другую перегрузку метода Operation с 24 строки. И сюда я передаю три параметра. Видите, как система определила. Первое – это строковой параметр, второй – int, третий double. Не этот же метод подставился. Потому что если я бы был компилятором я бы понял, что мне нужно определить из имеющихся методов, они все одноименные, теперь мы начинаем определять по параметрам. Далее смотрим, в следующем методе, который принимает один аргумент у нас снова же происходит вызов метода с 24 строки, его четвертая перегрузка. И мы теперь видим, что в качестве первого аргумента, мы подставляем единственный аргумент этого метода. На 19 строке мы создаем еще одну перегрузку метода operation с двумя параметрами, и видим, что здесь снова подставляются два аргумента этого метода и вызывается перегрузка с 24 строки. Давайте смотрим дальше. Мы на 31 строке вызываем первую перегрузку метода. Он в свою очередь вызывает четвертую перегрузку на 32 строке мы вызываем вторую перегрузку, он в свою очередь вызывает, обратите внимание, четвертую. На 33 строке мы вызываем третью, а он в свою очередь вызывает четвертую. Какой запутанный код. У нас здесь по сути имеется перегрузка метода, к которой обращаются все. Мы видим что к одной перегрузке обращаются другие перегрузки. Не очень красиво, как-то запутанно, непонятно, надо смотреть на эти подсветки, учитывать количество аргументов, надо смотреть на подсветку синтаксиса. Вот кликаем по нему, на какой строке этот метод. Правда не очень удобно. Не удобно, посмотри что нам предлагает, как же нам предлагает Microsoft.
1: using System; 2: 3: // Методы с опциональными параметрами. 4: 5: namespace Methods 6: { 7: class Program 8: { 9: // Метод вызывается так же, если бы это были перегрузки. 10: static void Operation(string value1 = "val", int value2 = 10, double value3 = 12.2) 11: { 12: Console.WriteLine("{0}, {1}, {2}", value1, value2, value3); 13: } 14: 15: static void Main() 16: { 17: Operation(); // 1-я перегрузка. 18: Operation("val"); // 2-я перегрузка. 19: Operation("val", 10); // 3-я перегрузка. 20: Operation("val", 10, 12.2); // 4-я перегрузка. 21: 22: 23: // Нельзя поставить третий параметр value3 = 12.2, вместо второго параметра value2 = 10! 24: // 12.2 не может быть приведено к int — здесь C# пытается оставить по умолчанию третий параметр, а не второй. 25: //Operation("val", 12.2); // ОШИБКА! 26: 27: // В данном случае возможно использование именованных параметров. 28: // Они состоят из указания имени параметра, двоеточия и значения, которое мы передаем. 29: 30: Operation("val", value3: 12.2); 31: Operation(value2: 33, value3: 12.2); 32: 33: 34: // Delay. 35: Console.ReadKey(); 36: } 37: } 38: }
Обратите внимание, на 10 строке мы уже создаем только один метод Operation. Если у нас здесь было их 4, и вот этот метод Operation… Давайте мы попробуем их немножко сличить. Смотрите, вот они. И здесь у нас уже используются работа с опциональными параметрами. И если вы зайдете в презентацию, вот они опциональные параметры. Мы здесь уже используем опциональные параметры и вот опциональные параметры позволяют нам избавится от множества перегрузок. И сейчас мы посмотрим, как мы избавляемся при помощи опциональных и даже при помощи немножко именованных параметров от множественных перегрузок. На 10 строке мы создаем метод Operation с тремя опциональными параметрами, или тремя опциональными аргументами. Первому параметру value1 мы присваиваем значение по умолчанию «val», value2 присваиваем значение по умолчанию 10, value3 присваиваем значение по умолчанию 12,2. Когда мы вызываем Operation и ничего не передаем, это аналог первой перегрузки из предыдущего примера. Когда мы вызываем метод Operation и используем здесь только один параметр, это аналогично второй перегрузке, потому что вот этот и этот параметр выполнится автоматически и мы здесь можем взять и заменить val на что-то другое. Допустим «First», потом «Second», здесь «Third». Мы вызываем метод Operation с 10 строки. «val» мы заменяем на F, но value2 и value3 так и остается, оно остается значениями по умолчанию. На 19 строке мы еще раз вызываем метод Operation с 10 строки. В качестве первого аргумента мы передаем S, а в качестве второго мы передадим 11. Помните чему равно число 11 в двенадцатеричной системе? 13. Плохое число. И мы смотрим что теперь в нашем случае у нас вот сюда в value1 подставится S в value2 подставится 11, а вот value3 останется равно 12,2. Здесь мы можем поставить при третьем вызове 22 и 3,14. Вот у нас пошел аналог четвертой перегрузки. Смотрите, как гибко позволяют нам работать с методом опциональные параметры. Наличие опциональных параметров позволяет не указывать их при вызове метода, потому что они уже имеют некие значения по умолчанию, которые будут подставляться в тело метода, а тело метода представляет собой набор инструкций, содержащих в себе рабочий код. И вот мы видим, что сюда подставляется третий, сюда второй… Сюда подставляется 12,2, сюда поставляется 10, сюда подставляется value1. И теперь если мы вот это вызываем, вызываем метод и подставляем только один параметр, что у нас получится? У нас выведется F, 10, 12.2. А если мы вызываем следующий метод с 19 строки, то здесь у нас будет S, 11, 12.2. В третьем случае, как мы понимаем подставится все три пользовательских. Давайте посмотрим еще на некоторые ограничения. Убираем комментарий с 25 строки. Возникает ошибка. Почему ошибка? Давайте посмотрим. Смотрите сколько всего вывелось! Иногда сложно проанализировать большой набор ошибок. Мы посмотрим первую. Cannot define overloaded method 'Function' because it differs from another method only on ref and out. Переходим и смотрим. Это вообще осталось со старого примера. Подходит она нам? Не подходит, но мы помним. А почему они остались, потому что мы не перебилдили наше решение. И потому если мы сейчас зайдем, поставим комментарий и нажмем F6. Перебилдили, возвращаемся. У нас снова появляется ошибка. Заходим в List Error. Ага, уже меньше стало. По крайней мене нас интересует 25 строка именно этого файла 004_Method. У нас есть множество, даже старые ошибки. Поэтому будьте осторожны, но не пугайтесь этого Error List. Вот то что нас интересует. Argument 2 cannot convert from 'double' to 'int'. Второй аргумент, а второй аргумент у на какой? Int. И поэтому система думает, что вы хотите первым аргументом поставить строку «Alex» и она подставится в этот параметр, а вот этот я пытаюсь подставить в value2. Это double параметр 12,2. Давайте вспомним кастинг. Что такое кастинг? Преобразование значений типов. Кастинг бывает явным и неявным, опасным и неопасным. В данном случае здесь используется опасный кастинг, потому что здесь теряется точность числа. Обрежется вот эта двойка, поэтому я не могу тебе преобразовать его. А что же мне делать? Обратите внимание, он не хочет преобразовывать. Давайте посмотрим еще вот так. А если я укажу явное преобразование значения типа к int. А сейчас нажимаем F6 и ошибок нет. То есть получается мне нужно учитывать что здесь работаю именно со вторым параметром, а мне вот показалось, что я работаю с третьим. Мне неправильно показалось. Многие могут думать что здесь искусственный интеллект, он сам подумает и подставит куда нужно. Так не получится. Имеется такое вот правило. Поэтому мы можем либо вот так подставить int сюда как решение этой проблемы. То есть использовать оператор явного преобразования значения типа. То что я рассказывал, вы можете здесь в комментариях тоже прочитать. И теперь смотрим, чтобы все-таки хоть как-то организовать правильно этот вывод. Мы делаем вот что. Обратите внимание, первым параметром указываем строку «Alex», попадает вот сюда это значение. И здесь мы указываем, используем технику… Если вот эта техника создания методов называется техника опциональный параметров, то при вызове метода – техника именованных параметров. Значит мы совместно с техникой опциональных параметров используем технику именованных параметров. Мы здесь явно указываем имя параметра, которому мы хотим присвоить 12,2. Здесь же у нас получается… Мы здесь не указываем… И здесь что получается? Во второй параметр вставится значение по умолчанию. То есть запомните пожалуйста это правило. И смотрим дальше. А если я хочу упустить имя. Например, здесь по умолчанию имя User, я хочу упустить имя. Если я попробую без именованных параметров, то снова же будет ошибка. Поэтому я должен явно указать имена тех параметров, которым я хочу присваивать значения. Даже если я просто хочу их поменять местами. Почему бы и нет, давайте попробуем. Берем и вставляем сюда. Вот пожалуйста, мы поменяли их местами. Но так как у нас используется техника именованных параметров, то здесь мы можем их перепутывать как хотим. Но что мы с вами знаем, что этим злоупотреблять не следует, потому что нам с вами нужно писать чистый код с правильными именованиями, с правильными последовательностями. Он может нам только помочь декорировать наш код, сделать его удобочитаемым, потому что правило отдавать предпочтение удобочитаемости вашего кода. Чтобы другие его тоже читали. Если выбирать между быстродействием и удобством, мы выбираем удобство. Потому что коды промышленные и вы где-то сэкономите и еще неизвестно как эта сомнительная производительность, действительно ли она будет, а мет она не совсем будет. Поэтому мы всегда отдаем предпочтение чистому удобочитаемому коду. Хорошо. Мы переходим с вами дальше.
Здесь мы немного поговорим о методе Main. Часто задают вопросы о том, как работает метод Main. Мы должны понимать что в языке C# метод Main – это фиксированное имя, в каждой программе за исключением некоторых, мы эти программы разберем дальше, используется метод с именем Main. Этот метод по сути является точкой входа в нашу программу. Что значит точка входа? Когда мы запускаем нашу программу на выполнение то виртуальная машина начинает ее читать, и ей нужно найти место, с которого можно начать читать. С какой строки ее читать. С 23? Или, например если у нас много методов, как в предыдущих примерах. С какого метода мне начать ее выполнять. Так вот была разработана такая семантика, такой смысл, что именно метод Main будея являться главной основной точкой входа в программу. И когда наша программа будем выполнятся мы подразумеваем, что все выполнение начинается с метода Main. И когда мы нажимали F11, мы становились на первую строку, где открывающая операторная скобка этого метода и с нее начиналось все выполнение. Замечательно. Ну, на самом деле если сильно постараться, конечно на C# невозможно такого сделать, но на промежуточном языке MSIL можно назначить любой метод точкой входа, но на C# такого сделать невозможно и нам приходится довольствоваться методом Main. Именно с большой буквы, потому что мы помним, что язык С# Case Sensitive. Метод Main может возвращать только два типа значений: int и void. Давайте посмотрим, что написано здесь в комментариях.
1: using System; 2: 3: // Возврат значений из метода Main () 4: 5: // По завершении программы имеется возможность вернуть значение из метода Main () вызывающему процессу или операционной системе. 6: 7: // Возвращаемое значение метода Main() может быть только типа int. 8: 9: // Как правило, значение, возвращаемое методом Main (), указывает на нормальное завершение программы, 10: // или на аварийное ее завершение из-за сложившихся ненормальных условий выполнения. 11: // Условно нулевое возвращаемое значение обычно указывает на нормальное завершение программы, 12: // а все остальные значения обозначают тип возникшей ошибки. 13: 14: namespace Methods 15: { 16: class Program 17: { 18: static int Main() 19: { 20: Console.WriteLine("Hello world!"); 21: 22: // Delay. 23: Console.ReadKey(); 24: 25: return 0; 26: } 27: } 28: }
Возврат значений из метода Main(). По завершении программы имеется возможность вернуть значение из метода Main() вызывающему процессу или операционной системе. Что это значит? Вызывающему процессу. Вот давайте как раз об этом и поговорим. Что такое процесс? Раньше, еще до такого активного использования операционных систем Windows мы программы называли программами. Вот несу я дискету с программой. Вставляю ее в дисковод. Запускаю. Программа попадает в память, в ОЗУ. И она программа как на дискете, так и во время выполнения в ОЗУ. Но приходит ОС Windows и говорит, что больше не хочет что мы программы называли программами. Когда вы несете программу по Windows на дискете вы должны называть ее приложением – Application. Это приложение к ОС Windows. Когда вы запускаете, оно оказывается в оперативной памяти, процессор начинает его выполнять. Мы не хотим, чтобы вы это приложение называли приложением когда оно в ОЗУ. Мы хотим чтобы его называли процессом. Смотрите, какие термины. До Windows программы на дискете и программы в ОЗУ. Пришла Windows, программу на дискете мы начали называть – Application – приложение, а программу во время выполнения мы называем процессом. ОС – это большая программа, которая нас обслуживает, которая позволяет запускать другие программы и когда мы запускаем какую-то программу, такое приложение под ОС Windows она оказывается в ОЗУ, то такие программы во время выполнения называются процессами. Так вот оказывается что одни программы, могут вызывать другие программы. Почему бы и нет. Одна программа вызывает другую программу, для того, чтобы другая программа для первой программы выполнила какую-то деятельность. Например. Почему нет. Так вот а результат этой деятельности нужно получить? Или успешность/неуспешность выполнения. Или что там вообще она делает. Вот вы программа, которая пишет программы – вы программисты. Но вы не умеете делать дома ремонт. Поэтому что вы делаете? Вы вызываете другого человека-программу, которая приходит и красит потолки, шлифует паркет, ставит стеклопакеты. Заметьте, и вы к ней периодически обращаетесь и говорите. Значит так, я пошел на работу, а ты сегодня мне сделай ремонт и вечером отдашь мне результат, или скажешь успешно или не успешно сделал мне ремонт. Такие у нас с вами шуточные примеры. Поэтому, обратите внимание, мы должны при создании нашей программы предусмотреть, что нас может вызвать. Другая программа может двойным щелчком мыши запустить нашу программу и получить возвращаемое значение из нашей программы. Обратите внимание, если другая программа нас запускает, либо ОС нас может запустить, потому что ОС это тоже программа. Она рассчитывает получить какое-то возвращаемое значение. Давайте посмотрим, возвращаемое значение метода Main может быть только типа int. Все, оно либо void, либо int. Как правило, значение возвращаемое методом Main указывает на успешное завершение программы, либо на аварийное ее завершение. Что это значит. Ага, если мы возвращаем 0 другой программе, мы говорим: «Я не смогла нормально выполнится, потому что мне для нормального выполнения не хватало таких-то файлов, не хватало доступа в интернет или метеосервис был отключен, ну чего-то мне не хватило.» Эта программа понимает, что вторая программа не выполнила работу. Вы понимаете, что у вас ремонт не сделали и вам сегодня придется ночевать у друзей, к примеру. Вы не можете идти домой, потому что вам вернулся 0. Представляете. Либо если вам возвращается 1, это значит что строители вам звонят и говорят, что они закончили ремонт и вы можете идти и ночевать в своей квартире. Так же и наша программа, которую вызовет другая программа должна ей сказать либо она выполнила деятельность, либо по каким-то причинам не выполнилась. Вот у строителей не хватало водки или не хватало материалов, не успели сделать. Хорошо. Как правило, значение, возвращаемое методом Main (), указывает на нормальное завершение программы, или на аварийное ее завершение из-за сложившихся ненормальных условий выполнения. Условно нулевое возвращаемое значение обычно указывает на нормальное завершение программы, а все остальные значения обозначают тип возникшей ошибки. Вы возвращаете 0 если успех. Возвращаете 1 – не было водки. Возвращаете 2 – не было плитки. 3 – не успели подвести стеклопакет. 4 – машина для шлифовки паркета сломалась. 5 – обвалился потолок. 6 – провалился пол. 7 – разрушилась стена. 8 – взрыв в туалете. 9 – затопило. Видите сколько типов ошибок. И мы понимаем, что 0 – это все прекрасно. Хорошо. Думаю что с такой простейшей теорией вам достаточно понятно, а мы потихоньку будем двигаться дальше. Смотрим, допустимы ли перегрузки методов Main? Мы понимаем, что система, на системный загрузчик с целевой платформой, вместе с CLR будет искать точку входа по методу Main, и по специальному методу Main, него должно быть возвращаемое значение. Давайте мы попробуем создать здесь еще один метод Main.
1: using System; 2: 3: // Возврат значений из метода Main () 4: 5: // Перегрузка метода Main () - допустима. 6: 7: // Точкой входа в программу может быть метод Main (), который возвращает значение типа void или int. 8: 9: namespace Methods 10: { 11: class Program 12: { 13: 14: // Перегрузка. (Не является точкой входа) 15: static string Main(string argument) 16: { 17: return "Hello " + argument + "!"; 18: } 19: 20: 21: // Точка входа в программу. 22: static int Main() 23: { 24: string @string = Main("World"); 25: 26: Console.WriteLine(@string); 27: 28: // Delay. 29: Console.ReadKey(); 30: 31: return 0; 32: } 33: } 34: }
Мы видим, что у нас имеется Main с возвращаемым значением string. CLR скажет, что не будет этот брать. Ей нужно чтобы у Main был либо int, либо void. Давайте попробуем еще один. Static void Main() {} Сделали. Смотрим. У нас теперь 2 метода. Выполняем. Type 'Methods.Program' already defines a member called ‘Main’ with the same parameter type. Параметры не нравятся. Хорошо, давайте попробуем сделать какой-нибудь параметр. Int a. Почему бы и нет. Билдимся… Успешно. Как вы думаете что у нас выполнится. Скорее всего вызовется вот этот метод. Давайте проверим. Берем и выводим на экран сообщение, например «void Main». Выполняемся. Смотрите, вывелся Hello World! Вот этот метод, и вот этот не вызвались. Почему? До этого мы видели, мы еще с вами массивы не проходили, но у нас был метод с массивом строковых аргументов была такая конструкция. Мы сейчас ее напишем быстро. Был вот такой метод. Давайте попробуем сейчас выполнится. Появилась ошибка. Вот мы и подловили нашу CLR. Вот, проблемы с Entry Point – это точка входа. Compile with /main to specify the type that contains the entry point. Есть только два метода Main, которые могут содержать точку входа. Обратите внимание, хитрость, массив строковых аргументов. Мы к этому массиву подойдем попозже. Но теперь мы видим, что имеется две точки входа в систему и для программы теперь это коллизия. Он говорит, что вы можете оформить только вот такой метод, либо вот такой метод. Вот это не является точкой входа в программу. У нас появилось две точки входа. А не возможно сразу начать выполнять программу с двух мест. Поэтому мы должны отдать предпочтение либо этой точке входа, либо этой точке входа. Но это мы сами уже тут накуролесили. Давайте закомментируем этот блок. Выполняемся. Void Main. Сработала вот эта точка входа. Закомментируем вот эту. Выполняемся. Выполнился Hello World! Выполнился другой метод Main. Поэтому просто запомните себе что метод Main в большинстве случаев является точкой входа в программу. Да, мы можем делать определенные перегрузки этого метода, но зачем. Мы понимаем, что имя метода должно отражать род выполняемой им деятельности. Я не знаю что можно мне придумать даже в маленькой программке, чтобы у меня было два метода Main тем более две точки входа. Они недопустимы. Ну и мы понимаем, что точки входа могут отличатся чем? Методом Main и его возвращаемыми значениями. А если вот здесь поставить массив строковых аргументов? Давайте попробуем и вот здесь тоже поставим массив строковых аргументов, потому что и так система не хочет различить их. И так, что с точки зрения перегрузок это разные методы, но по какой части метода исполняющая среда определяет перегрузку метода? По возвращаемому значению. Вот по чему идет перегрузка. Вот это не учитывается. Мы можем сюда подставить. Система определяет перегрузку метода Main, точки входа по возвращаемому значению. Но это немного сложно и запутанно. Но может вы повторно прослушаете еще раз этот эпизод с этими примерами и возможно у вас будет больше понимания. У вас есть возможность несколько раз прослушивать эти примеры. Хорошо. Мы здесь тогда закомментируем, и вы себе закомментируйте, пусть оно останется. У меня все равно тестовый пример и я его после курса удалю. Выполняемся и смотрим. Hello World! Переходим к следующему примеру.
И следующая важная техника, которая используется при работе с методами – это рекурсия. Что же такое рекурсия? Скажу одним словом – самовызов. Когда метод из своего тела вызывает сам себя. Когда метод обращается сам к себе по имени. Вот представьте, что я сижу и сам себя зову. Рекурсивный вызов. Я сам себя вызвал. Не вы меня зовете, а я сам себя. Самовызов. И мы сейчас с вами разберем как эта техника работает, у нее есть свои определенные особенности. Давайте посмотрим сначала на слайд. Обратите внимание, у нас имеется метод с именем Recursion. В теле метода Main мы вызываем этот метод, и передаем ему параметр 2. Мы понимаем, что counter теперь равен 2. В теле метода мы уменьшаем значение аргумента на 1. Выводим на экран counter. Вот мы вывели единицу на экран. Далее мы проверяем в условной конструкции counter != 0. Условие удовлетворяет истинности и мы входим в тело условной конструкции и еще раз пытаемся вызвать себя, то есть еще раз вызывает себя, но уже в качестве аргумента мы уже передаем единицу. Counter = 1. Что происходит в этот момент. Внимание эффект. У нас в памяти выделается область. Здесь система скопипастила и вставила такой же самый метод. Обратите внимание, мы переходим теперь куда? На копию. Мы вот здесь прыгаем сюда на копию. Мы видим, что counter чему при вызове равен был? 1. Уже работаем с копией. Мы отсюда уже ушли. Мы вот здесь остановились. Тут еще кусок программы. Подождите. Работаем с копией. Counter = 1. Уменьшаем его на 0 и теперь он будет равен 0. На оригинале он остается равным единице. Видите? А в копии counter чему равен? 0! Два одновременно существующих блока в памяти. Оригинал и копия. Здесь counter остается равным 1. Здесь он равен 0. Мы выводим на экран 0. Далее, проверяем в условной конструкции if counter != 0. False. И мы что? Мы обходим ее, не заходим. Мы в нее не зашли. И что мы видим? Снова же на экран. Counter чему равен в этой копии. 0. Смотрите, мы выводим второй ноль. Мы завершили работу метода. Только мы завершили работу метода, мы сразу переходим на следующую команду после вызова этого метода. Мы вернулись в оригинал из копии. Копии нет. Она свое дело сделала. Смотрим, а чем здесь равен counter в оригинале? 1! И мы выводим на экран 1. Обратите внимание, как работает рекурсия. Самовызов. Во время рекурсивного вызова, когда мы внутри имеющегося метода, имеющейся функции обращаемся к своему же имени. Вызываем себя. Что у нас происходит? Происходит построение копии этого метода и переход на эту копию. При этом оригинал. Остается временно замороженным. С ним ничего не происходит, все его переменные остались такими, какие они есть. Вы скажете: «Интересно, а как же метод узнал… Ладно мы перешли сюда, выполнили его и вернулись обратно вот сюда. Как же вот это этому методу стало известно, что надо перейти именно вот сюда?» А дело в том, что каждый раз, после перехода на новый метод мы в специальной области памяти, которая называется стеком, сохраняем адреса возврата. Адреса возврата из вызванных нами методов. Мы знаем, что каждая инструкция имеет свой адрес, просто она скрыта и мы ее не видим. Значит стек – это область памяти для хранения адресов возврата из процедур. Скажу что наша программа строится примерно вот так. Любая программа строится по вот такому принципу. Сначала идут все данные в программе. Дальше идет блок с кодом, а дальше идет стек. Область стека. Потому что здесь идет вся работа с данными. Здесь находятся коды. Здесь изменяющиеся данные. А вот здесь стек, в котором учитывается в первую очередь адреса возврата из вызванных методов. Потому что мы можем из одного метода вызвать другой, потом еще один вызвать, потом еще… У нас такие длинные цепочки методов, а потом мы довыполнились, идем дальше по программе, вызываем еще методы. И вот эти вызовы методов контролируются, то есть мы контролируем возврат. Что нам напоминает стек в жизни. Представьте себе глубокое подземелье. Много этажей, как гиперкуб, например. Много этажей и много комнат. Представьте, что там имеется миллион комнат. И в этих катакомбах, в этом сложном лабиринте есть вход. Все комнаты пронумерованы. Представьте себе. Каждая комната имеет табличку с номером. Там еще много этажей и мы вас берем и говорим: «Так, мы вам сейчас отправляем в лабиринт искать золото, там лежат клады золота в каких-то комнатах, мы не знаем в каких. Что вам з собой нужно?» Вы скажете, что вам нужен рюкзак, чтобы бросить туда тушёнки, воды, путь достаточно долгий, фонарик, свечки, спички. А главное что? Чтобы не заблудится. Это же лабиринт, и люди могут заблудится в лабиринтах. Мы и так здесь вам помогли. Мы дали лабиринт из изначально пронумерованными комнатами и с каждой комнаты по несколько входов и выходов есть. Веревка? Во-первых, если ее возьмёте с собой, такой моток веревки – это очень тяжело будет. Если веревка станется здесь, вы не потянете эту катушку. Комнаты пронумерованы. Вам понадобится листочек и ручка. Заходите вы в первую комнату и на листочке пишите 1. Переходите через несколько входов в другие комнаты. Заходите в третью, пишите 3. Номер смотрите и пошли дальше по лабиринту. Идете, идете, идете, идете и тут ваш листочек заканчивается, вы записали последнее значение. Писать больше некуда. Вы мне по радиосвязи говорите, что у вас закончился листочек. Я говорю, чтобы вы шли дальше. Но это бессмысленно, если я сделаю еще пару шагов в соседние комнаты. Все, я потеряю выход, потому что я сейчас собрался идти в обратном порядке. Каким путем шел, таким и буду выходить, чтобы мне заблудится. Но если я сейчас пойду дальше, то я заблужусь, и не факт, что я выйду на какую-то из этих комнат. Хорошо, возвращайтесь. Идете вы обратно. Прошли там три комнаты и тут срывается с потолка сталактит, падает и пробивает вам листочек посередине. В листочке теперь дырка. Что вы делаете? Вы садитесь и начинаете плакать. Звоните и говорите, что у меня поврежден стек. У вас поврежден стек. Область адресов возврата разрушилась. Поэтому и в ОС повреждение стека или Stack Overflow Exception – переполнение стека, считается одной из самых серозных и опасных исключений. И специалисты считают, что после повреждения стека восстанавливать его нет смысла. То есть мне вас ждать нет смысла. Я конечно обращусь в соответствующие органы, в службу спасения, что вот вы где-то заблудились в лабиринте. Но, к сожалению, вероятность того, что программа после повреждения стека может быть восстановлена до работоспособного состояния очень и очень низкая. В программировании мы даже не пытаемся восстановить разрушенный стек, потому что это считается все, финиш. Это большая проблема. Так вот, как в жизни с лабиринтом, так и в программировании есть тоже такой листок бумаги, только это не листок бумаги, а страница памяти, потому что когда мы работаем в защищенном режиме, то у нас память разбивается не на сегменты, а на страницы памяти. У них есть разные размеры, несколько размеров, но стандартный размер страницы памяти, которая выделяется под работу со стеком равна 1 мегабайту. Обратите внимание, если мы здесь вызовем миллион раз сами себя и попытаемся построить миллион копий, у нас быстро закончится листочек и в итоге мы получим Stack Overflow Exception. Ну там гораздо меньше, мы сейчас не считаем размер адреса, мы это сейчас не считаем. Помимо адресов возврата стек может хранить еще кое что, но мы это не смотрим. Нас интересует стек, как область памяти для хранения адресов возврата из вызываемых нами процедур толи обычным способом вызываются разные процедуры. Толи рекурсивно. Замечательно. Мы еще раз закрепим, зайдем в Visio. Обратите внимание, вот идет наша программа. Идет оригинал программы, вот начальное значение counter, которое мы с вами рассматривали. Представьте, что у нас в качестве аргумента передается не 2, а 3. Как у нас в данном случае пойдет работа. Вот мы идем на условие, и если условие удовлетворяет истинности, то мы идем на его копию, видите, здесь строится его копия, здесь опять условие и до тех пор, пока условие не перестанет удовлетворять истинности у нас будут создаваться копии. Я почему показываю, это очень символично. Здесь у нас показана вот такая елочка. И мы видим что у нас постоянно идут переходы на копии, на копии и мы вот так выполняемся и возвращаемся и возвращаемся по стеку. Обратите внимание, вот эти числа 2, 1, 0. Представьте, что это вывод на экран. Давайте мы так немножко сдвинемся. И видим, что здесь мы выводим двойку. Counter = 2. Переходим дальше – выводим 1. Потом 0. И потом мы начинаем выполнять вторую половину метода. То есть, смотрите, мы выполняем только первую половину метода, ту что до самовызова. А здесь у нас идет то, что после самовызова. Когда мы уже возвращаемся у нас довыполняются части наших методов. Как копий, так и оригинала. Вот видите, по такому принципу у нас идет вызов и дальше, когда мы уже возвращаемся, здесь выводится 0. Здесь выводится 1, потому что эта копия хранила в себе 1. Здесь выводится 2. Уже с оригинала. А мы сейчас с вами возвращаемся в код и смотрим.
1: using System; 2: 3: // Рекурсия (простая рекурсия). 4: 5: // В теле метода Recursion на 19-й строке рекурсивно вызывается метод Recursion. 6: // Простая рекурсия - вызов методом самого себя (самовызов). При каждом вызове строится новая копия метода. 7: 8: namespace Methods 9: { 10: class Program 11: { 12: static void Recursion(int counter) 13: { 14: counter--; 15: 16: Console.WriteLine("Первая половина метода: {0}", counter); 17: 18: if (counter != 0) 19: Recursion(counter); 20: 21: Console.WriteLine("Вторая половина метода: {0}", counter); 22: } 23: 24: static void Main() 25: { 26: Recursion(3); 27: 28: // Delay. 29: Console.ReadKey(); 30: } 31: } 32: }
На 12 строке мы создаем метод с именем Recursion, которые принимает один целочисленный аргумент и в теле у нас организована какая-то логика. На 26 строке мы вызываем этот метод. Будем шагать? Конечно будем. Начинаем выполнение с метода Main, помните, точка входа в программу. Вызываем Recursion и передаем 3. Counter = 3. Это первый вызов, сейчас мы работаем с оригиналом. Если мы посмотрим. То мы находимся сейчас где? Вот здесь видите. Так 3. Уменьшаем counter на 1 и он равен 2. Далее, проверяем counter != 0. Конечно же нет. Вызываем себя. И во время вызова себя что у нас происходит? Построение вот этой второй копии. Видите, мы сейчас в копии находимся. Видите, курсор сейчас стоит вот здесь, на копии. Так, уменьшаем counter и он будет равен 1. Вот эти блоки проверки условия. Так, 1 != 0 = True. Создаем еще одну копию. Смотрите, мы уже вот здесь. Вот она копия. Counter пока 1. Выполняемся. Теперь он равен 0. Обратите внимание, у нас работают первые половины методов. Первая половина метода 2. Первая половина метода 1. Первая половина метода 0. Это у нас шел оригинал, первая копия, вторая копия. Так, counter != 0 – false. Мы зайдем сюда? Нет, не зайдем. Шагаем дальше. Смотрите, отрабатывает вторая половина метода, вторая половина второй копии. Мы сейчас отработали вот здесь. Дальше. Переходим в код. Смотрим. Куда мы вернулись? Вот куда мы вернулись. Мы находимся здесь сейчас. И сейчас у нас выведется что? 1. Смотрите, counter = 1. Выводим 1. Мы сейчас в оригинале. Counter = 2. Мы уже находимся в оригинале. Еще раз, обратите внимание на этот слайд, как мы прошлись рекурсивно по методу. Рекурсия – это самовызов. Вот вы видите тот вывод, который у нас получился в нашем примере. Видите то что мы с вами выполняли. Вот как у нас отработала рекурсия. Что нам напоминает рекурсия? Рекурсия нам напоминает некие циклы. Какие-то циклически выполняемые действия. Просто? Просто. А мы идем дальше. Обратите внимание, Здесь мы с вами разберем понятие сложной рекурсии. Давайте зайдем в презентацию и вспомним такую вещь. Смотрите, у нас имеется первый метод и имеется второй метод. Мы понимаем, что мы из тела одного метода можем вызывать другие методы. Из одного метода мы вызываем другой метод. Но иногда бывает так, что мы можем вызывать себя через другой метод. Если до этого мы смотрели как сами же вызываем себя, то в сложной рекурсии я не могу сказать Саша. Я говорю: «Иван, позови пожалуйста меня.» И он зовет меня. Давайте посмотрим, как это выглядит в коде.
1: using System; 2: 3: // Рекурсия (сложная рекурсия). 4: 5: // Сложная рекурсия - вызов методом себя, через другой метод. 6: 7: namespace Methods 8: { 9: class Program 10: { 11: static void Recursion(int counter) 12: { 13: counter--; 14: 15: Console.WriteLine("Первая половина метода Recursion: {0}", counter); 16: 17: if (counter != 0) 18: Method(counter); 19: 20: Console.WriteLine("Вторая половина метода Recursion: {0}", counter); 21: } 22: 23: static void Method(int counter) 24: { 25: Console.WriteLine("Первая половина метода Method: {0}", counter); 26: 27: Recursion(counter); 28: 29: Console.WriteLine("Вторая половина метода Method: {0}", counter); 30: } 31: 32: static void Main() 33: { 34: Method(3); 35: 36: // Delay. 37: Console.ReadKey(); 38: } 39: } 40: }
На 11 строке мы создаем метод Recursion, снова же тот же самый counter, но в условной конструкции if, если условие удовлетворяет истинности мы уже вызываем другой метод. А этот метод уже безусловно вызывает наш метод, он вызывает нас. Вы видите да? И получается, что здесь мы вызываем вот этот вот метод из нашего рекурсивного метода. А он будет перевызывать нас. Видите как идет здесь вызов. Давайте попробуем пошагать эту программу. Мы вызываем метод 3 раза, вот у нас идет Method(3). Мы переходим в метод. Count = 3. Отработала первая половина метода, вызывается метод Recursion. Здесь мы уменьшаем счетчик на 1 и снова же вызываем вот этот метод. А он снова же вызывает нас. Мы снова же вызываем его, а он вызывает нас. Мы снова вызываем этот метод. А он вызывает нас. То есть теперь мы пошли выполнять вторые части этих методов видите? И если мы посмотрим в код, то сначала видим, что отработала первая половина метода Method. Потом первая половина метода Recursion, потом первая половина метода Method и т.д. А потом идут дорабатывать вторые половинки. Вот эта техника называется сложной рекурсией, когда мы вызываем себя через другой метод. Видите, самовызов через другой метод. Думаю что с рекурсией мы достаточно разобрались, мы еще будем возвращаться к этому понятию, еще будем с ним работать. А мы идем с вами дальше. И мы уже подходим к окончанию нашего урока. И под конец мы рассмотрим три маленьких примера использования рекурсии. Обратите внимание, этот первый пример показывает нам вывод числа в двоичном формате, которое мы передаем в десятичном формате. Вот например у нас имеется число в десятичном формате 12. Как это число выглядит в 16-ричном формате? Это С = 1100. Вот это число, как оно у нас выведется. Давайте выполнимся.
1: using System; 2: 3: // Вывод в двоичном формате числа, переданного в десятичном формате 4: 5: namespace BinaryDigit 6: { 7: class Program 8: { 9: static void Converter(int n) 10: { 11: int temp; 12: 13: temp = n % 2; 14: 15: if (n >= 2) 16: Converter(n / 2); 17: 18: Console.Write(temp); 19: } 20: 21: static void Main() 22: { 23: int n = 69; 24: 25: Converter(n); 26: 27: Console.ReadKey(); 28: } 29: } 30: }
Обратите внимание, вот, мы десятичное число 12 преобразовали в двоичный формат, при этом… Давайте сейчас попробуем пошагать. Вот у нас происходит вызов метода Converter. Значит n = 12. Смотрим, остаток равен 0. N больше либо равно 2. Да. Что сейчас произойдет? Построится копия. А вы помните формулы перевода чисел из десятичного формата в двоичный, помните польскую запись числа. Мы рассматривали ее на машинной математике. Если вы откроете этот пример, сосредоточитесь на нем, пошагаете, то вы здесь ясно увидите работу рекурсии. И мы здесь давайте уже дошагаем. Мы здесь шагаем первую половину метода, теперь переходим выводить остатки. А как мы остатки выведем? В обратной записи. Помните польскую запись числа. Польская нотация. Обратный вывод. Мы вот так вот делили, а потом из остатков получали число. И мы здесь сейчас можем посмотреть, сейчас должно вывестись. Смотрите, у нас вывелась первая единица с последней копии. Еще одна единица вывелась. Сейчас у нас выведется что? 0. И последний ноль остался в оригинале и вот мы его получили. Здесь вам нужно сосредоточиться взять этот пример, пошагать, посмотреть пример из машинной математикой и посмотреть как работает польская запись числа. То есть мы взяли и вот так рекурсивно преобразовали десятичное число в двоичное. Еще какие способы есть использования рекурсии.
1: using System; 2: 3: // Нахождение наибольшего общего делителя (НОД) двух целых чисел 4: 5: namespace Recursion 6: { 7: class Program 8: { 9: static int Calculate(int a, int b) 10: { 11: if (a % b == 0) 12: return b; 13: else 14: return Calculate(b, a % b); 15: } 16: 17: static void Main() 18: { 19: Console.WriteLine("Нахождение наибольшего общего делителя двух целых чисел"); 20: 21: int a = 15, b = 33; 22: 23: Console.WriteLine("a = {0}, b = {1}, НОД = {2};", a, b, Calculate(a, b)); 24: 25: Console.ReadKey(); 26: } 27: 28: #region Второй вариант решения 29: 30: static int Calculate2(int a, int b) 31: { 32: while (b != 0) 33: b = a % (a = b); 34: return a; 35: } 36: 37: #endregion 38: } 39: }
Ага, нахождение наибольшего общего делителя двух целых чисел. Что такое НОД. Помните, мы в школе учили это понятие. Наибольшим общим делителем для двух целый чисел m и n называется наибольший из их общих делителей. Например для чисел 70 и 105 наибольший общий делитель равен 35. Числа большего, чем 35 у нас не имеется. И название идет не просто общий делитель. Потому что общий делитель для 70 и 105 например 5. А именно наибольший общий делитель. Так вот, для изыскания НОДа мы тоже можем использовать рекурсию в функциях. Обратите внимание, здесь идет первый способ решения этой задачи и второй способ решения этой задачи. Один и второй. Я вам рекомендую ее тоже пошагать этот пример и поизучать его. Открыть вот эту страницу на Википедии посмотреть что же такое НОД и согласно вот этих формул попробовать пошагать. Еще где может использоваться давайте посмотрим. Скорее всего это факториал. Давайте смотрим. Здесь у нас идет расчет факториала.
1: using System; 2: 3: // Нахождение факториала числа. 4: 5: namespace MethodsRecFact 6: { 7: 8: class Program 9: { 10: static int Factorial(int n) 11: { 12: if (n == 0) 13: return 1; 14: else 15: return n * Factorial(n - 1); 16: } 17: 18: static void Main() 19: { 20: int factorial = Factorial(5); 21: 22: Console.WriteLine(factorial); 23: 24: // Задержка. 25: Console.ReadKey(); 26: } 27: } 28: }
До этот мы рассчитывали факториал с помощью циклов do-while. И факториал… Помните это понятие? Что мы там находили? Мы находили возможное количество вариаций доставки книги. Также здесь, только мы уже не используем цикл, а используем также рекурсию. И соответственно в копиях будут сохранятся промежуточные вычисления значений факториала. Потому что мы знаем, что если мы хотим вычислить 4! = 24. Мы понимаем, что мы начнем разбивать ряд на копии, и натуральный ряд чисел до 4 будет разбит по каждой из копий рекурсивно вызванных методов ну и дальше произойдет на возврате, мы будем n умножать на возвращаемое значение из построенной копии. Пошагайте. Попробуйте разрисовать эти программы так же как мы разрисовывали вот так лесенко с использованием копий на небольших числа. Тот же факториал 4 постройте. Напишите копии, опишите их и вы сможете хорошо для себя закрепить работу с рекурсией. Значит мы подошли к концу нашего урока. Те темы, которые мы с вами разбирали: перегрузка методов, разновидности аргументов, и дальше мы подошли к достаточно большому разделу - это к рекурсии. Я думаю, что рекурсия вам понятна после окончания этого урока, главное запомнить, что рекурсия строит некую копию метода и выполняется лесенкой. Сначала выполняются все первые половинки. Потом выполняются все вторые половинки. Мы разобрали понятие стека и поговорили много о чем интересном. На этом мы заканчиваем наш урок. Спасибо за внимание. До новых встреч.