Безумно короткий самоучитель по C#. Часть 1 из 2.

Добавлен , опубликован
Раздел:
C#
Этот самоучитель:
  • Без разжевывания
  • Поверхностный
  • Не полный
Если хотите - жуйте подробности в комменты, задавайте вопросы где "совсем не понятно".
Материал чутка связан, потому если вы встретили в коде что-то непонятное - читайте дальше, наверняка будет ответ.
Прошу прощения за очень грубое и поверхностное объяснение. На самом деле именно в нем и есть соль, что знания можно "подхватить". А как вы с ними будете работать дальше другой вопрос. Так же как и возможные конструкции - не все.
Пусть это будет пособие для студентов ПТУ, которые вчера жевали семки, а сегодня решившие МНЕ СРОЧНО НУЖНО ОСВОИТЬ ЯЗЫК ПРОГРАММИРОВАНИЯ. Короче таких как я :D
Итак, поехали:

Типы

C#, это не паскаль, там главное - типы.
Типы очень грубо делятся на:
  • типы значений (структуры, struct)
  • ссылочные типы (классы, class)
  • интерфейсы (interface)
  • перечисления (enum, но под капотом они сводятся к struct)
Есть несколько базовых типов. Самые часто юзаемые это:
  • int (целое число) Пример 0, 10, 10i
  • float (дробное число) Пример 0.1, .1f, 10f
  • char (символ) Пример 'a', 'b', 'c'
  • string (строка, массив символов) Пример "строка", @"другая строка"
  • bool (true/false, да/нет, переключатель) Пример true, false
  • byte (байт, по сути тоже целое число, но с маленьким диапозоном)
  • double (дробное число поточнее)
  • object (общий тип)
Свои же типы состоят из вот этих базовых и других созданных типов:
class Яблоко
{
    Color цвет;
    float вес;
}
Чем типы значений отличаются от ссылочных типов?
В первом случае (типы значений) идет сравнение объектов по содержимому.
То есть из примера выше - два яблока будут равны, если их цвет и вес совпадет.
Во втором случае (ссылочные типы) идет сравнение по ссылке.
То есть два яблока будут равны только если это одно и тоже яблоко, независимо от того, совпадает их содержимое или нет.

Инкапсуляция

Инкапсуляция обеспечивает уровень доступа какой-то сущности (типа, метода, поля, свойства) и выражается одним из четырех ключевых слов.
  • public (обшедоступно)
  • private (можно использовать только внутри класса)
  • internal (можно использовать только внутри текущей сборки/библиотеки)
  • protected (можно использовать только внутри класса или в классе-наследнике)
  • protected internal (можно использовать внутри текущей сборки/библиотеки хоть где, а вне сборки только для классов-наследников)
Эти слова пишутся соответственно перед инициализацией всех классов/полей/методов
То есть:
public class Яблоко
{
    public Color цвет;
    internal float вес;
}
Если ключевое слово у поля отсутствует, то оно по умолчанию считается за private.

Наследование

Наследование помогает не переписывать общие данные внутри классов 20 раз.
Пишется через двоеточие после названия класса (указываются классы/интерфейсы).
На самом деле интерфейсы это контракт и их не наследуют, а реализуют, это допущение для краткости, т.к. с точки зрения синтаксиса это очень похожие вещи.
То есть:
public class Фрукт
{
    public Color цвет;
    internal float вес;
}

public class Яблоко : Фрукт
{
    public bool червивое;
}

public class Томат : Фрукт
{
    public int количествоСемян;
}
В этом случае и яблоки и томат будут иметь общие поля фруктов - цвет и вес.
Класс может пронаследоваться максимум от одного другого класса.
Любой тип, в том числе класс может реализовывать любое количество интерфейсов.

Полиморфизм

Полиморфизм это свойство объекта представляться любым из пронаследованных типов.
Например из примера выше мы можем описать поля так:
public class MyClass
{
    public Фрукт фрукт; //В такое поле мы можем положить любой фрукт, будь он хоть яблоко, хоть груша
    public Яблоко яблоко; //Сюда можно положить только яблоки
    public object чтоУгодно; //В поле с типом object мы можем положить что угодно, это общий тип у всего
}

Метод

Метод, функция, процедура - почти синонимы.
Почему почти
Процедура - метод, который ничего не возвращает.
Функция - метод, который что-то возвращает.
Метод - функция/процедура.
В сути же своей в разговорной речи как вы не называйте это - все вас прекрасно поймут. В данной статье я возможно даже пару раз перепутал эти понятия.
В методах вы можете писать специфичный для вашего объекта код.
Например:
public class Яблоко
{
    private bool червивое;
    public void СделатьЧервивым()
    {
        червивое = true;
    }
} 
Там где указан 'void' подставляется возвращаемый тип. Если функция просто выполняется и не должна отдавать никакое значение, ставится void
Пример функции возвращающей значение:
public class Яблоко
{
    private bool червивое;
    public bool ПроверитьЧервивость()
    {
        return червивое;
    }
}
'return' возвращает значение переменной.
После выполнения 'return' функция обрывается, код при срабатывание 'return' не выполняется дальше.
В круглых скобках указываются параметры. Например:
public class Яблоко
{
    private bool червивое;
    private float вес;
    public void НазначитьСвойства(bool червивое, float вес)
    {
        this.червивое = червивое;
        this.вес = вес;
    }
}
Слово this позволяет обратиться к полям экземпляра, когда в самой функции уже есть переменные с таким названием.

Конструктор и деструктор

Для создания объекта вы можете использовать специальный метод - конструктор, а при очистке срабатывает метод деструктор
Конструктор не указывает возвращаемый тип - тупо пишется название типа, а затем параметры.
public class Яблоко
{
    public float вес;
    public Яблоко(float вес) 
    {
        this.вес = вес;
    }
}
Такой тип можно создать c помощью ключевого слова new в любом методе
var яблоко = new Яблоко(.25f);
Если у класса не указаны конструкторы то считается что у него есть пустой конструктор без параметров. Такой класс можно создать вот так:
var яблоко = new Яблоко();
Деструктор выполняется когда объект уничтожается. Объект уничтожается автоматически если нигде не используется и не хранится.
public class Яблоко
{
    ~Яблоко()
    {
        Debug.Log("Яблоко съедено!");
    }
}

Инициализация

Помимо использования конструктора вы можете указать для полей значения по умолчанию прямо после объявления.
public class Яблоко
{
    public float вес = .2f;
}

Свойства

Свойство это гибрид поля и метода. Он нужен когда при присвоении значения или его возврате нужно выполнить какие-то дополнительные операции. Так же хорошим тоном считается помечать как поля кешируемые переменные.
По сути свойства это синтаксический сахар - можно обойтись без них, но они удобные.
Развернутый вид выглядит например вот так:
public class Яблоко
{
    private float _вес;
    
    public float вес 
    {
        get { return _вес; }
        private set { _вес = value; }
    }
}
Без свойств это же самое можно было сделать так
    private float _вес;
    
    public float get_Weight 
    {
         return _вес; 
    }
    private void set_Weight (float value)
    {   
        _вес = value; 
    }
Ключевое слово value позволяет присвоить значение.
Данный пример показывает как свойство "вес" можно узнать откуда угодно, но изменить только внутри класса.
Функция set может отсутствовать вовсе:
public class Яблоко
{
    private float _вес;
    
    public float вес 
    {
        get { return _вес; }
    }
}
Помимо развернутой конструкции есть "свернутый вариант":
public class Яблоко
{
    public float вес { get; set; }
    public float вес2 { get; private set; }
}
В свернутом варианте мы не можем указать доп код и использовать поле само по себе, но он удобен когда нам просто нужно изменить уровень доступа.

Статичность

Бывает случаи когда какая-то часть класса относится не к одному объекту, а ко всем сразу.
Такие сущности помечаются словом static
public class Яблоко
{
    public Color цвет;
    public Яблоко() 
    {
        всеЯблоки.Add(this);
    }
    
    ~Яблоко() 
    {
        всеЯблоки.Remove(this);
    }

    public static List<Яблоко> всеЯблоки = new List<Яблоко>;
    public static void УдалитьВсеЯблоки() 
    {
        всеЯблоки.Clear();        
    } 
}
Такие сущности существуют не на уровне экземпляра, а на уровне класса.
Вы так же можете помечать классы как статичные. В этом случае внутри смогут быть только статичные поля и функции, а сам класс нельзя будет создать.
Для сравнения
Доступ к полю экземпляра "цвет":
var яблоко = new Яблоко();
яблоко.цвет = Color.green;
Доступ к статичному полю "всеЯблоки":
Яблоко.всеЯблоки.Clear();

Абстрактность, перезапись, интерфейсы

Бывает нужным, чтобы наследники обязательно реализовывали какой-то метод.
Есть несколько способов этого достичь.
1 способ.
Пометить класс как абстрактный.
public abstract class Фрукт
{
    public abstract void СьестьФрукт(); //Никаких фигурных скобок!
}

public class Яблоко : Фрукт 
{
    public void СьестьФрукт() //Если класс не будет содержать такой метод то компилятор выведет ошибку
    {
        //Какой то код    
    }
}
Абстрактный класс нельзя создать через new. Только его наследников.
2 способ.
Пометить поля как возможные к перезаписи.
public class Фрукт
{
    public virtual void СьестьФрукт() //Помечаем словом virtual 
    {
     
    }
}

public class Яблоко : Фрукт 
{
    public override void СьестьФрукт() //Помечаем словом override
    {
        //Какой то код    
    }
}
При перезаписи вы можете вызвать метод-родитель
Например
public class Фрукт
{
    public virtual void СьестьФрукт() //Помечаем словом virtual 
    {
        //Здесь какой нибудь код      
    }
}

public class Яблоко : Фрукт 
{
    public override void СьестьФрукт() //Помечаем словом override
    {
        base.СьестьФрукт(); //Выполнить код у родителя 
        //Какой то другой код     
    }
}
Такой подход не выдаст ошибку если наследуемый класс не реализует метод. В этом случае выполнится код класса, от которого пронаследовались.
3 способ.
Интерфейсы - это такая конструкция, которая обязывает вас реализовывать методы.
По сути работает так же как абстрактный класс, но не позволяет создавать поля и указывать уровень доступа (он всегда считается либо как паблик, либо видим только на уровне интерфейса). Можно реализовать несколько интерфейсов на один класс.
Пример:
public interface IФрукт
{
    void СьестьФрукт(); 
}

public class Яблоко : IФрукт
{
    public void СьестьФрукт() //Если класс не будет содержать такой метод то компилятор выведет ошибку
    {
   
    }
}

Код алгоритмов

Код алгоритмов здесь (такого понятия вообще т нет) - это код который выполняется ВНУТРИ функции. Ниже речь пойдет о том что там можно делать.

Объявление переменной

Переменные можно создавать в любой части кода.
Класс не помеченный как статичный способен создавать экземпляры. Создание переменной происходит с помощью слова new
Яблоко яблоко = new Яблоко(); 
В этом примере мы указываем явный тип "Яблоко" для переменной "яблоко" и затем создает объект с помощью дефолтного конструктора без параметров.
В таких очевидных случаях возможно не указывать тип явно, а просто подставить ключевое слово 'var'
var яблоко = new Яблоко();
Однако в некоторых случаях указать тип нужно, например когда мы хотим представить объект в другом типе
Фрукт яблоко = new Яблоко();
Так же можно объявлять переменные полей и свойств. Например:
public class Рука
{
    public object предметВРуке;
    public void ВзятьЯблоко() 
    {
        предметВРуке = new Яблоко();        
    } 
}

Значения по умолчанию

Так же есть значение null обозначающее "ничего". Используется, например, для очистки значений.
null могут присвоить только интерфейсы, классы или объект типа object.
Фрукт фрукт = new Яблоко();
фрукт = null;
//Теперь фрукт равен null
При инициализации переменных у них есть значения по умолчанию. Например вот такая конструкция:
Фрукт фрукт;
float вес;
фрукт в этом случае равен null, так как это ссылочный тип и значение не было указано.
вес равен 0, так как типы значений не могут быть равны null.

Обращение к полям и свойствам

Вы можете обратиться к полям и свойствам любого объекта:
Яблоко яблоко = new Яблоко();
яблоко.вес = .1f; //Обращение к полю 'вес'
К статичным полям обращаются по названию класса:
Яблоко.всеЯблоки = null;

Обращение к методам

яблоки.Add(new Яблоко());

Условия, циклы, контекст

Условия и циклы всем известные конструкции, объяснять их смысл, думаю, не шибко нужно.
Примеры условий:
if (условие)
{
    //Действия
}

if (условие)
{
    //Действия
}
else
{
    //Действия
}

if (условие)
{
    //Действия
}
else if (условие)
{
    //Действия
}
Примеры циклов:
while (условие)
{
    //действия
}

do
{
    //действия
}
while (условие);

for (int i = 0; i < 4; i++)
{
}
В качестве условия выступает любое выражение, возвращающее тип bool (true/false)
Любой цикл можно прервать командой 'break' и продолжить выполнение действий дальше, после цикла.
Можно начать "новый виток цикла" командой 'continue'
Внутри круглых скобок цикл for делится на три части
  1. Выполняется перед стартом цикла
  2. Условие выхода из цикла
  3. Действие в конце каждого цикла
Если после условия/цикла не стоит фигурных скобок (контекста) значит будет выполнено следующее действие после конструкции. Например:
if (условие)
    //Действие  
Переменные, созданные в рамках контекста, если никуда не записываются - уничтожаются по окончанию контекста. Переменные "живут" текущим контекстом или контекстом выше.
Пример
if (условие)
{
    var яблоко = new Яблоко();
    {
        яблоко.вес = .1f; //Здесь нет ошибки, так как текущий контекст существует внутри контекста, где переменная существует
    }
}
яблоко.вес = .1f; //Здесь ошибка, так как переменная "яблоко" создана в другом контексте.

Массивы и списки

Массив описывается квадратными скобками. Например
Яблоко[] яблоки = null;
Объявление массива происходит с указанием длины массива:
Яблоко[] яблоки = new Яблоко[10];
Есть так же прямоугольные и зубчатые массивы.
Пример прямоугольного массива:
float[,] матрица4х4 = new float[4,4];
Это означает что будет создан массив 4 на 4 (как таблица). Количество измерений может быть любым
Пример зубчатого массива:
var зубчатыйМассив = new Яблоко[10][];
Это значит, что первое измерение имеет длину 10. Вторая размерность определяется для каждого объекта персонально. Например:
var зубчатыйМассив = new Яблоко[10][];
зубчатыйМассив[0] = new Яблоко[12];
Если вы не знаете точной длины вы можете использовать списки. Они имеют динамичную длину. Списки представлены классом List<T>
Пример:
var список = new List<Яблоко>();
список.Add(new Яблоко());
Примеры доступа к длине
Обычный массив - array.Length
Прямоугольный массив - array.GetLength(n) для длины конкретного измерения
Список - list.Count

Объявление с контекстом

Во время объявления можно заюзать фигурные скобки чтобы указать присвоение каких-то данных. Это чисто синтаксический сахар.
Пример:
var список = new List<Яблоко> { new Яблоко(), new Яблоко() };
var массив = new [] { new Яблоко(), new Яблоко() };
var объект = new Яблоко { вес = 0.1f };

Операторы сравнения

В C# есть следующие операторы для сравнения (в примере):
a == b //равно
a != b //не равно
a >= b //больше либо равно
a <= b //меньше либо равно
a > b //больше
a < b //меньше
Используются для получения выражений типа bool, в том числе условий

Арифметические операторы

a + b
a * b
a - b
a / b
a % b //Остаток от деления

Операторы присвоения

Есть следующие операторы присвоения (в примере):
a = b; //Присвоить а значение b
a += 2; //Увеличить a на 2, аля a = a + 2; 
a -= 2;
a *= 2;
a /= 2;
a %= 2;

Приведение к типу

Вы можете привести объект к определенному типу
Пример конструкций:
var яблоко = new Яблоко();
var фрукт = (Фрукт)яблоко; //Приводим к типу "Фрукт". В случае невозможности привести тип - ошибка
var фрукт = яблоко as Фрукт; //Приводим к типу "Фрукт". В случае невозможности привести тип - запишется null. Можно делать только с типами поддерживающими null-значение.
var этоФрукт = яблоко is Фрукт; //Проверяем, можно ли привести объект к типу "фрукт". Возвращает true или false

Инкремент и декремент

Выглядят они например вот так: i++, i--, ++i, --i.
По сути это вычитание или прибавление единицы к любой числовой переменной.
Разница в порядке выполнения на примере (надеюсь не намудил):
var i = 1;
var a = i++; //Сначала присвоить a = i, затем прибавить i на 1. В итоге a = 1, i = 2
 
var i = 1;
var a = ++i; //Сначала прибавить i на 1, затем присвоить a = i. В итоге a = 2, i = 2

i++; //Просто прибавить i на 1
++i; //Просто прибавить i на 1
Что я упустил из основ и сам знаю что это упустил:
  • дженерик типы
  • ref/out
  • пространства имен
  • методы расширения
  • using
  • лямбда-выражения
  • делегаты
  • события
  • рефлексия
  • операторы в классах
  • индексаторы
Возможно, в следующей серии, если эта статья затащит ;)
Код строчил в блокноте, если где-то намудил - исправляйте