Самоучитель по C# для начинающих. 02. Функции, классы, обьекты, коллекции

2.1 Функции.

Вернемся к старой задаче - выводе данных о людях. У нас есть разные люди с данными в виде отдельных фамилии, имени, отчества, которые надо вывести на экран - вида Пушкин Александр Сергеевич и Пушкин А.С. Чтобы задача была правдоподобней можно имитировать ввод данных пользователем или загрузку из внешнего источника, но все это будет пустой тратой времени - реальные приложения все равно работают с графическим и/или веб-интерфейсом. Просто держим в уме что в реальности людей не два, а две тысячи и заранее их имена не известны.

Получаем уродливый код

            string name = "Александр";
            string otchestvo = "Сергеевич"; 
            string surname = "Пушкин";


            string name2 = "Наталья";
            string otchestvo2 = "Николаевна";
            string surname2 = "Гончарова";

            System.Console.WriteLine(surname  + " " + name + " " + otchestvo);
            System.Console.WriteLine(surname2  + " " + name2 + " " + otchestvo2); 

            System.Console.ReadLine();

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

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

Пример функции

        public static string CreateFio(string surname, string name, string otchestvo)
        {
            string fio = surname + " " + name + " " + otchestvo;
            return fio;
        }
 

Слова public static отложим на пару минут в сторону, string означает, что функция вернет назад строку, CreateFio(string surname, string name, string otchestvo) - название функции и описание того, что она принимает на вход три строки.

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

public static void CreateFio()
{
    // фио мы здесь явно создать не сможем, вечная морока с этим тестовыми примерами
}

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

Весь код тестовой программы с функциями

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestConsoleApplication
{
    class Program
    {
        public static string CreateFio(string surname, string name, string otchestvo)
        {
            string fio = surname + " " + name + " " + otchestvo;
            return fio;
        }

        public static string CreateFioInitials(string surname, string name, string otchestvo)
        {
            string fio = surname + " " + name.Substring(0, 1) + ". " + otchestvo.Substring(0, 1) + ".";
            //Временно оставим в стороне код "Substring(0, 1)" - он просто вырезает первый символ из строки. 
            return fio;
        }


        static void Main(string[] args)
        {


            string name = "Александр";
            string otchestvo = "Сергеевич"; 
            string surname = "Пушкин";


            string name2 = "Наталья";
            string otchestvo2 = "Николаевна";
            string surname2 = "Гончарова";

            System.Console.WriteLine(CreateFio(surname, name, otchestvo));
            System.Console.WriteLine(CreateFioInitials(surname, name, otchestvo));
            System.Console.WriteLine(CreateFio(surname2, name2, otchestvo2));
            System.Console.WriteLine(CreateFioInitials(surname2, name2, otchestvo2));

            System.Console.ReadLine();

        }

    }
}

2.2 Классы и объекты

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

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

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

А вот каждый конкретный человек со своими значениями ФИО будет экземпляром класса человек. Точно так же как каждый ящик со своими значениями высоты, ширины и глубины будет являться экземпляром класса ящик. Само собой у каждого класса может быть масса других параметров - например и у человека и у ящика может быть параметр масса, у человека может быть параметр пол - мужской или женский, а вот у ящика пол вряд ли будет (хотя кто знает, что нам готовит непредсказуемое будущее)

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestConsoleApplication
{
    class Program
    {
        
        static void Main(string[] args)
        {
            // создаем два экземпляра класса человек с разными фио
            Person person1 = new Person("Пушкин", "Александр", "Сергеевич");
            Person person2 = new Person("Гончарова", "Наталья", "Николаевна");

            // выводим разные фио используя встроенные в класс функции - методы
            System.Console.WriteLine(person1.GetFio());
            System.Console.WriteLine(person1.GetFioInitials());
            System.Console.WriteLine(person2.GetFio());
            System.Console.WriteLine(person2.GetFioInitials());
            //статическая функция одинакова для всех экземпляров класса и вызывается с имени самого класса
            System.Console.WriteLine(Person.GetClassDescription());

            System.Console.ReadLine();

        }

    }

    // класс человека
    public class Person
    {
        // три строковых переменные-свойства, доступные извне класса - public
        public string Name = "";
        public string Surname = "";
        public string Otchestvo = "";

        // конструктор, специальная функция, которая вызывается при создани экземпляра класса с помощью слова new
        public Person(string surname, string name, string otchestvo)
        {
            Name = name;
            Surname = surname;
            Otchestvo = otchestvo;
        }

        // этим функциям не надо подавать на вход данные - они используют данные экземпляра класса
        public string GetFio()
        {
            string fio = Surname + " " + Name + " " + Otchestvo;
            return fio;
        }

        public string GetFioInitials()
        {
            string fio = Surname + " " + Name.Substring(0, 1) + ". " + Otchestvo.Substring(0, 1) + ".";
            return fio;
        }


        // статическая функция не используют данные экземпляра, она одинакова для всех экземпляров. По сути дела класс для нее - просто название группы функций
        public static string GetClassDescription()
        {
            return "Класс Person. Хранит данные о человеке.";
        }

    }
}

В профессиональном программировании считается дурным тоном давать свободный и бесконтрольный доступ к хранимым в классе данным. Чтобы запретить доступ извне к свойству Surname для него надо прописать другой модификатор доступа, вместо public - private. Записывать и считывать данные надо будет через специальные функции, аналогичные тем функциям, которые на самом деле что-то вычисляют в духе GetFio. Чтобы не возиться с написанием кучи функций типа GetName и SetName в C# применяется специальный механизм свойств, на основании которых уже при компиляции программы автоматически генерятся эти однотипные функции. Такой способ облегчить жизнь программисту называется синтаксическим сахаром. В Visual Studio есть даже специальная функция для этого.

c-sharp-guide-007

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

Финальный код

    class Program
    {
        
        static void Main(string[] args)
        {
            // создаем два экземпляра класса человек с разными фио
            Person person1 = new Person("Пушкин", "Александр", "Сергеевич");
            Person person2 = new Person("Гончарова", "Наталья", "Николаевна");

            System.Console.WriteLine(person1.Fio);
            System.Console.WriteLine(person1.FioInitials);
            System.Console.WriteLine(person2.Fio);
            System.Console.WriteLine(person2.FioInitials);
            System.Console.WriteLine(Person.ClassDescription);
            System.Console.WriteLine(Person.ClassDescription);
        }
    }

    // класс человека
    public class Person
    {
        // три строковых переменные-свойства, доступные извне класса - public
        private string _name = "";
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        private string _surname = "";
        public string Surname
        {
            get { return _surname; }
            set { _surname = value; }
        }

        private string _otchestvo = "";
        public string Otchestvo
        {
            get { return _otchestvo; }
            set { _otchestvo = value; }
        }

        public string Fio
        {
            get
            {
                string fio = Surname + " " + Name + " " + Otchestvo;
                return fio;
            }
        }

        public string FioInitials
        {
            get
            {
                string fio = Surname + " " + Name.Substring(0, 1) + ". " + Otchestvo.Substring(0, 1) + ".";
                return fio;
            }
        }

        // конструктор, специальная функция, которая вызывается при создани экземпляра класса с помощью слова new
        public Person(string surname, string name, string otchestvo)
        {
            Name = name;
            Surname = surname;
            Otchestvo = otchestvo;
        }

        public static string ClassDescription
        {
            get
            {
                return "Класс Person. Хранит данные о человеке.";
            }
        }
    }

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

            Person person1 = new Person("Пушкин", "Александр", "Сергеевич");
            Person person2 = new Person("Гончарова", "Наталья", "Николаевна");

            System.Console.WriteLine(person1.Fio);
            System.Console.WriteLine(person1.FioInitials);
            System.Console.WriteLine(person2.Fio);
            System.Console.WriteLine(person2.FioInitials);
            System.Console.WriteLine(Person.ClassDescription);
            System.Console.WriteLine(Person.ClassDescription);

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

2.3 Структура программ реального мира. Пространства имен.

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

Для нашей тестовой программы мы используем пространство имен TestConsoleApplication, состоящее из двух классов - Program и Person.

Класс Program автоматически создается для каждого Net приложения, это стандартный класс. Как правило он состоит из одной функции/метода static void Main(string[] args), которая выполняется при запуске приложения.

Разработчики C# и .Net очень любят классы. Просто очень-очень. Так что в итоге здесь все является классами и даже функции без классов не существуют. Если вам не нужны ни классы ни экземпляры, и вы хотите просто сделать нескольких функций - вам все равно придется создать класс, в который добавить несколько статических (не требующих создания экземпляра) функций.

Классическим пример такой функции может послужить умножение

        public static int Multiply(int a, int b)
        {
            return a * b;
        }

Которое где-то в другом месте вызывается как

System.Console.WriteLine(SomeClassName.Multiply(2, 2));

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

Другой пример класса который по сути своей является библиотекой функций - класс Convert, содержащий функции для преобразования одних типов данных в другие

int testNum = Convert.ToInt32("34");

В .Net Framework входит множество уже готовых классов от программистов Microsoft, разделенных на множество пространств имен. Самые-самые базовые входят в пространство имен System.

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

System.Console.WriteLine("Привет мир!");

В данном случае мы вызываем функцию public static void WriteLine(string value) из класса Console входящего в пространство имен System. И уже где-то там внутри нее, скорее всего еще несколькими уровнями ниже выполняется рисование по пикселям окна с консолью и наших букв.

Чтобы не писать каждый раз пространство имен используется директива using

и на самом деле если мы уже прописали

using System;

то вывод текста в консоль можно было бы записать проще (помешали лень и копирование в буфер)

Console.WriteLine(person1.GetFio());

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

string fio = surname + " " + name.Substring(0, 1) + ". " + otchestvo.Substring(0, 1) + ".";

Здес мы вызываем один из методов класса String - public string Substring(int startIndex, int length), который возвращает кусок строки и принимает два аргумента, индекс символа, с которого надо начать вырезание подстроки (нумерация начинается с 0) и длину вырезаемой подстроки.

Если же нам не нужны методы вообще, просто хочется собрать в кучку данные - можно использовать структуры. С другой стороны можно создать специальный класс без функций, с одними данными - для этого есть умное название обьект для передачи данных (Data Transfer Object, DTO).

2.4 Особенности хранения данных в памяти. Ссылочные и простые типы данных. Область видимости переменных.

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

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

В C# дела обстоят иначе. Для самых простых типов данных память компьютера выделяется автоматически, написав int counter; мы автоматически получим место в памяти, в которое будет по умолчанию записан 0.

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

            string surname1 = "Пушкин ";
            string surname2 = "Гончарова";
            
            // в surname1 записывается значение surname2 но сами переменный продолжают существовать независимо
            surname1 = surname2;
            surname2 = "ААА";

            Console.WriteLine("Значение surname1: " + surname1); // Гончарова
            Console.WriteLine("Значение surname2: " + surname2); // ААА

            Person person1 = new Person("Пушкин", "Александр", "Сергеевич");
            Person person2 = new Person("Гончарова", "Наталья", "Николаевна");
            // в person1 записывается ссылка на обьект person2 теперь обе переменные ссылаются на один и тот же обьект класса Person
            person1 = person2;
            person2.Surname = "AAA";

            Console.WriteLine("Значение person1.Surname: " + person1.Surname); // ААА
            Console.WriteLine("Значение person2.Surname: " + person2.Surname);  // ААА
            Console.ReadLine();

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

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

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

            {
                int testVariable = 1;
            }

            testVariable = 2; // такая программа просто не скомпиллируется, прямо в VisualStudio выдаст ошибку The name 'testVariable' does not exist in the current context	

А вот так все будет в порядке.

            int testVariable = 1;
            {
                testVariable = 2;
            }

Другой вариант первой ошибки

            int testVariable = 1;
            {
                int testVariable = 2;
            }

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

             {
                int testVariable = 2;
                Console.WriteLine(testVariable);
            }

            {
                int testVariable = 2;
                Console.WriteLine(testVariable);
            }

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

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

2.5 Массивы, коллекции и цикл foreach

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

            int[] intArray = new int[5];
            int[,] multiDimensionalIntArray = new int[2, 3];

            Person[] personsArray = new Person[2];
            personsArray[0] = new Person("Пушкин", "Александр", "Сергеевич");
            personsArray[1] = new Person("Гончарова", "Наталья", "Николаевна");

            Console.WriteLine(personsArray.Count()); // выведет 2
            Console.WriteLine(personsArray[1].Fio); // выведет Гончарова Наталья Николаевна

            List<Person> personsList = new List<Person>();
            personsList.Add(new Person("Пушкин", "Александр", "Сергеевич"));
            personsList.Add(new Person("Гончарова", "Наталья", "Николаевна"));
            System.Console.WriteLine(personsList.Count()); // выведет 2
            System.Console.WriteLine(personsList[0].Fio); // выведет Пушкин Александр Сергеевич

            for (int counter = 0; counter <= 1; counter++)
            {
                Console.WriteLine(personsList[counter].FioInitials);
            } 

В чем же отличие массивов от списков? Подробности относятся к продвинутым знаниям, но в целом под массив место в памяти выделяется сразу и если надо изменить его размер, то место приходится выделять заново. Под список фиксированный объем не выделяется, он меняется динамически, но операции над элементами выполняются медленнее. Таким образом массив быстрее и лучше работает, если количество элементов не меняется, список - если меняется. Но в большинстве случаев эта разница в скорости не заметна на фоне других факторов. Продвинутые программисты хорошо понимают разницу между ArrayList, Dictionary, HashSet, LinkedList, Collection и другими типами коллекций.

Обходить все элементы в каком-то множестве приходится так часто, что в большинстве современных языков для этого сделали особый цикл, в C# это цикл foreach, "для каждого".

            foreach (Person currPerson in personsList)
            {
                // currPerson - currentPerson - текущий человек, код внутри цикла будет вызываться раз за разом и в этой переменной последовательно окажутся все элементы последовательности, для каждого из которых можно будет что-то сделать
                Console.WriteLine(currPerson.FioInitials);
            }

Выйти из цикла можно с помощью ключевых слов break - просто прекращается выполнение цикла и continue - пропускает текущую итерацию и переходит к следующей

            foreach (Person currPerson in personsList)
            {
                // пропускаем Пушкина и переходим к гончаровой
                if (currPerson.Surname == "Пушкин")
                {
                    continue;
                }
                Console.WriteLine(currPerson.FioInitials);
            }

            foreach (Person currPerson in personsList)
            {
                // цикл не сделает ничего, так как на первом шаге его выполнение прервется
                if (currPerson.Surname == "Пушкин")
                {
                    break;
                }
                Console.WriteLine(currPerson.FioInitials);
            }

Комментарии

Самоучитель по C# для начинающих. 02. Функции, классы, обьекты, коллекции — Комментарии (16)

  1. Код в конце 2.2 можно продолжать усовершенствовать,
    1) для начала отделить GUI(WriteLine()) от бизнес логики (Person)
    2) избавляться от зависимостей по принципам SOLID
    3) дойти до одного из паттернов MVC или MVVM
    4) придти к IoC 😀

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

      И сам не так уж давно с нуля осваивал, и со стороны много раз видел с каким скрипом даются первые шаги.

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

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

  2. public Person(string surname, string name, string otchestvo)
    {
    Name = name;
    Surname = surname;
    Otchestvo = otchestvo;
    }
    Поясни, пожалуйста этот пункт, примера, что здесь делается? Только начинаю учиться, поэтому частенько теряюсь:)

  3. Описывается конструктор класса, функция которая вызывает при создании экземпляра класса. Она принимает три переменные и записывает их в свойства класса.

    Конструкторы экземпляров (Руководство по программированию в C#)

    Конструкторы (Руководство по программированию на C#)

  4. public string FioInitials
    {
    get
    {
    string fio = Surname + " " + Name.Substring(0, 1) + ". " + Otchestvo.Substring(0, 1) + ".";
    return fio;
    }
    }
    Спасибо, ха предыдущий пункт, все стало понятно! Есть еще вопрос, по второму примеру, а, в этом блоке кода свойсвто гет зачем?

  5. Плут:
    public string FioInitials { get { string fio = Surname + " " + Name.Substring(0, 1) + ". " + Otchestvo.Substring(0, 1) + "."; return fio; } }
    Спасибо, ха предыдущий пункт, все стало понятно! Есть еще вопрос, по второму примеру, а, в этом блоке кода свойсвто гет зачем?

    Понятно, что он нужен, но не понятно что он делает

  6. Для чего нам нужно вот это в первом примере программы с классами? Ведь даже удалив эти строчки программа будет работать точно также.
    System.Console.WriteLine(Person.GetClassDescription());
    ---------------------------------------
    public static string GetClassDescription()
    {
    return "Класс Person. Хранит данные о человеке.";
    }

    • Смысл всех тестовых примеров не в том, чтобы они что-то делали, а в том, чтобы они иллюстрировали описанное в статье. В данном случае иллюстрируется отличие статической функции от обычных функций и свойств, в том числе с синтаксическим сахаром.

  7. 2.5
    int[] intArray = new int[5];
    int[,] multiDimensionalIntArray = new int[2, 3];

    Подскажите пожалуйста, для чего эти 2 строки. Код вроде и так работает.

    PS: Огромное спасибо за Вашу работу. Это лучшее пособие для начинающих, из тех что я встречал.

    • Просто показывают как создаются одно и много мерные массивы на самом простом примере. Практического смысла весь код из всех трех примеров не имеет - он нужен только для демонстрации.

  8. Спасибо за отличный самоучитель! Все доступно и понятно. Автору огромное уважение!

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

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *


*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>