Ниже не учебник, а только шпаргалка для разработчиков уже знакомых с основами C# .Net.
Шпаргалка содержит только вопросы "на базу". Вопросы вида "как бы вы спроектировали ...", "какие слои приложения ...", в шпаргалку не входят.
Форматирование кода
В примерах, для краткости, открывающая скобочка { не на новой строке. Интервьюер может быть смущен, т.к. в C# принято ставить { с новой строки. Поэтому на собеседовании лучше использовать общепринятое форматирование.
stack и heap, value type и reference type
reference type (пример class, interface) хранятся в heap
value type (пример int, struct, ссылки на инстансы reference type) хранятся в быстром stack
при присвоении (передачи в метод) value type копируются, reference type передаются по ссылке (см. ниже раздел struct)
struct
value type => при присвоении (передачи в метод) все поля и свойства копируются, не может быть null
нет наследования
поддерживает интерфейсы
если есть конструктор, в нем должны устанавливаться все поля и свойства
interface IMyInterface { int Property { get; set; } } struct MyStruc : IMyInterface { public int Field; public int Property { get; set; } } class Program { static void Main(string[] args) { var ms = new MyStruc { Field = 1, Property = 2 }; // при передаче в метод value type копируется, // поэтому в методе будет другая переменная TryChange(ms); Console.WriteLine(ms.Property); // ==> ms.Property = 2; // тут происходит boxing (см ниже) IMyInterface msInterface = new MyStruc { Field = 1, Property = 2 }; // поэтому в метод передается уже object (reference type) // внутри метода будет не другая переменная, а ссылка на msInterface TryChange(msInterface); Console.WriteLine(msInterface.Property); // ==> ms.Property = 3; } static void TryChange(IMyInterface myStruc) { myStruc.Property = 3; } }
DateTime это struct, поэтому проверять поля типа DateTime на null бессмысленно:
class MyClass { public DateTime Date { get; set; } } var mc = new MyClass(); // всегда false, // т.к. DateTime это struct (value type) не может быть null var isDate = mc.Date == null;
Нельзя сделать структуру ссылающуюся на себя:
// НЕ КОМПИЛИРУЕТСЯ struct MyStruct1 { public MyStruct1? Prev; } // и так тоже НЕ КОМПИЛИРУЕТСЯ struct MyStruct1 { public MyStruct2? Prev; } struct MyStruct2 { public MyStruct1? Prev; }
boxing / unboxing
// boxing (value type, stack -> object, heap) int i = 123; object o = i; // unboxing (object, heap -> value type, stack) object o = 123; var i = (int)o;

// пример boxing int i = 123; object o = i; i = 456; // результат ==> т.к. i, o хранятся в разных ячейках памяти // i = 456 // o = 123
Зачем это нужно
// При приведении структуры к интерфейсу происходит boxing IMyInterface myInterface = new MyStruct(2); // boxing i int i = 2; string s = "str" + i; // т.к. это String.Concat(object? arg0, object? arg1) // unboxing, т.к. Session Dictionary<string, object> int i = (int)Session["key"];
string особенный тип
хранятся в heap как reference type, передаются в метод как reference type
string s1 = "str"; void methodStr(string str) { Console.WriteLine(Object.ReferenceEquals(s1, str)); // ==> true // string reference type, str и s1 указывают на один объект } methodStr(s1); int i1 = 1; void methodInt(int num) { Console.WriteLine(Object.ReferenceEquals(i1, num)); // ==> false // int value type, i1 и num это разные объекты } methodInt(i1);
строки не изменяемы (immutable) - каждое изменение создает новый объект
из-за того, что строки immutable и при сравнении сравниваются их значения (стандартное поведение reference type сравнивать ссылки) строки по поведению похожи на value type
из-за immutable при склеивании длинных строк нужно использовать StringBuilder
const vs readonly
const - значение подставляется при компиляции => установить можно только до компиляции
readonly - установить значение можно только до компиляции или в конструкторе
class MyClass { public const string Const = "some1"; public readonly string Field = "some2"; } var cl = new MyClass(); Console.WriteLine(MyClass.Const); Console.WriteLine(cl.Field);
Фокус-покус с подкладыванием dll библиотеки, без перекомпиляции основного проекта:
ref и out
ref и out позволяют передавать в метод ссылки на объекты, и для value type и для reference type
static void Main(string[] args) { int i = 1; Change1(i); // передаем значение i Console.WriteLine(i); // ==> i = 1 // передаем ссылку на i Change2(ref i); Console.WriteLine(i); // ==> i = 2 } static void Change1(int num) { num = 2; } static void Change2(ref int num) { num = 2; }
ref и out позволяют внутри метода использовать new и для class и для struct
out тоже что ref, только говорит о том что, метод обязательно пересоздаст переменную
struct MyStruc { public int Field; } class Program { static void Main(string[] args) { var ms = new MyStruc { Field = 1 }; createNew(ms); Console.WriteLine(ms.Field); // ==> ms.Field = 1 var ms2 = new MyStruc { Field = 1 }; createNew2(ref ms2); Console.WriteLine(ms2.Field); // ==> ms2.Field = 2 } static void createNew(MyStruc myStruc) { myStruc = new MyStruc { Field = 2 }; } static void createNew2(ref MyStruc myStruc) { myStruc = new MyStruc { Field = 2 }; } static void createNew3(out MyStruc myStruc) { // ошибка компиляции, // нужно обязательно использовать myStruc = new } }
Ковариантность
List<> инвариантен -> можно привести к переменной только того же типа
public class List<T>IEnumerable<> ковариантен -> часное можно привести к более общему
interface IEnumerable<out T> - указан outAction<> контрвари��нтен -> общее можно привести к частному (пример ниже)
delegate void Action<in T> - указан in
Термин встречается только при обсуждении generic-ов.
interface IAnimal { } class Cat : IAnimal { public void Meow() { } } class Dog : IAnimal { public void Woof() { } } // НЕ КОМПИЛИРУЕТСЯ, List<> - инвариантен // не компилируется, потому что у List есть метод Add, // который приводит к коллизиям (пример коллизии см. ниже) List<IAnimal> animals = new List<Cat>(); // компилируется, IEnumerable<> - ковариантен // у IEnumerable нет методов приводящих к коллизиям IEnumerable<IAnimal> lst = new List<Cat>(); // компилируется, Action<> - контрвариантен Action<IAnimal> actionAnimal = (cat) => { Console.WriteLine("работает"); }; Action<Cat> actionCat = actionAnimal; actionCat(new Cat());
К каким коллизиям приводит метод Add в List:
// это компилируется и работает List<Cat> cats = new List<Cat>(); cats.Add(new Cat()); List<Cat> animals = cats; animals.Add(new Cat()); foreach (var cat in cats) { cat.Meow(); // в cats 2 кошки } // это НЕ КОМПИЛИРУЕТСЯ List<Cat> cats = new List<Cat>(); cats.Add(new Cat()); List<IAnimal> animals = cats; animals.Add(new Dog()); // это не порядок, потому что: // перебираем foreach (var cat in cats) { cat.Meow(); // в cats 1 кошка и 1 собака, у собаки нет метода Meow() }
Публичные методы Object
ToString
GetType
Equals
GetHashCode
Про ToString и GetType спрашивать нечего.
GetHashCode
GetHashCode НЕ возвращает уникальный ключ/хеш объекта. Разные объекты, даже одного типа, могут возвращать одинаковое значение - и это будет корректно.
Соответственно, GetHashCode нельзя использовать для сравнения объектов (только как вспомогательную функцию).
GetHashCode нужен для быстрого поиска в хеш-таблицах. Такие объекты как HashSet<T> и Dictionary<TKey, TValue> используют в своей работе хеш-таблицы.
Если объект используется в качестве ключа в хеш-таблице, то значение его GetHashCode указывает на позицию в хеш-таблице. При этом на одной позиции может быть несколько разных элементов (у которых одинаковый GetHashCode).
Псевдокод поиска в хеш-таблице:
1. вначале ищем позицию в хеш-таблице
2. среди элементов этой позиции ищем элемент используя Equals
// Calculate the hash code of the key H = key.GetHashCode() // Calculate the index of the bucket where the entry would be, if it exists bucketIndex = H mod B // Enumerate entries in the bucket to find one whose key is equal to the // key we're looking for entry = buckets[bucketIndex].Find(key)
Два элемента в словаре с одинаковым GetHashCode:
class MyClass { public int Id; public override int GetHashCode() { return Id; } } var dic = new Dictionary<MyClass, string>(); dic.Add(new MyClass { Id = 1 }, "one"); dic.Add(new MyClass { Id = 1 }, "two"); Console.WriteLine(dic.Count); // ==> 2
Определим Equals
class MyClass { public int Id; public override int GetHashCode() { return Id; } public override bool Equals(object obj) { return ((MyClass)obj)?.Id == Id; } } var dic = new Dictionary<MyClass, string>(); dic.Add(new MyClass { Id = 1 }, "one"); dic.Add(new MyClass { Id = 1 }, "two"); // <== ИСКЛЮЧЕНИЕ
Статья зачем нужен GetHashCode:
https://thomaslevesque.com/2020/05/15/things-every-csharp-developer-should-know-1-hash-codes/
События, делегаты
class MyClass { public event Action<string> Evt; public void FireEvt() { if (Evt != null) Evt("hello"); // Evt("hello") - на самом деле перебор обработчиков // можно сделать тоже самое вручную //foreach (var ev in Evt.GetInvocationList()) // ev.DynamicInvoke("hello"); } public void ClearEvt() { // отписать всех подписчиков можно только внутри MyClass Evt = null; } } var cl = new MyClass(); // подписаться на событие cl.Evt += (msg) => Console.WriteLine($"1 {msg}"); cl.Evt += (msg) => Console.WriteLine($"2 {msg}"); // подписаться и отписаться Action<string> handler = (msg) => Console.WriteLine($"3 {msg}"); cl.Evt += handler; cl.Evt -= handler; cl.FireEvt(); // ==> // 1 hello // 2 hello // это НЕ КОМПИЛИРУЕТСЯ // на событие можно подписаться "+=" или описаться "-=" // отписать всех подписчиков можно только внутри MyClass cl.Evt = null;
Finalizer ~
вызывается когда garbage collector доберется до объекта. Можно указать GC не вызывать finalizer для определенного instance - GC.SuppressFinalize.
вызывается только автоматически средой .Net, нельзя вызвать самостоятельно (если очень хочется можно вызвать с помощью reflection).
нельзя определить для struct
зачем может пригодиться переопределять finalizer: предпочтительней реализовать IDisposable. Встречаются идеи дублировать логику Dispose в finalizer, на случай если клиентский код не вызывал Dispose. Или вставлять в finalizer логирование времени жизни объекта для отладки.
throw vs "throw ex"
try { ... } catch (Exception ex) { // это лучше, т.к. не обрезается CallStack throw; // обрезает CallStack throw ex; }
Garbage collector
Коротко. heap большая, но все же имеет ограниченный размер, нужно удалять неиспользуемые объекты. Этим занимается Garbage collector. Деление объектов на поколения нужно для следующего:
выявление есть ссылки на объект (объект используется) или его можно удалить - это трудозатратная задача
поэтому имеет смысл делать это не для всех объектов в heap
те объекты которые создали недавно (Generation 0) - вероятно это объекты используемые внутри метода, при выходе из метода они не нужны их можно удалить. Поэтому вначале искать объекты на удаление нужно в поколении Generation 0.
те объекты которые пережили сборку мусора - называют объектами Generation 1.
если Generation 0 почистили, а памяти не хватает. Приходится искать ненужные объекты среди тех которые пережили сборку - в Generation 1.
если все равно нужно еще чистить, ищем среди тех кто пережили 2 сборки мусора - в Generation 2.
Порядок инициализации
Derived.Static.Fields
Derived.Static.Constructor
Derived.Instance.Fields
Base.Static.Fields
Base.Static.Constructor
Base.Instance.Fields
Base.Instance.Constructor
Derived.Instance.Constructor
class Parent { public Parent() { // нельзя вызывать virtual в конструкторе // конструктор базового класса вызывается // раньше конструктора наследника DoSomething(); } protected virtual void DoSomething() { } } class Child : Parent { private string foo; public Child() { foo = "HELLO"; } protected override void DoSomething() { Console.WriteLine(foo.ToLower()); //NullReferenceException } }
Наследование в клиентском коде часто порождает излишнюю сложность (по мои наблюдениям, мое частное мнение), поэтому его нужно избегать. Для повторного использования кода лучше применить агрегацию (или, что хуже, композицию) - см. Наследование vs Композиция vs Агрегация.
ООП
Абстракция - отделение идеи от реализации
Полиморфизм - реализация идеи разными способами
Наследование - повторное использование кода лучше реализовать с помощью агрегации или, что хуже, композиции)
Инкапсуляция - приватные методы
SOLID
Single responsibility - объект, метод должны заниматься только одним своим делом, в противоположность антипатерну God-object
Open closed principle - для добавления новых функций не должно требоваться изменять существующий код
Liskov substitution - использовать базовый класс не зная о реализации наследника
Interface segregation principle - не раздувать интерфейсы
Dependency inversion principle - вначале интерфейсы, потом реализация, но не наоборот
Паттерны
Делятся на 3 типа
порождающие (пример: фабрика)
структурные (пример: декоратор)
поведенческие (пример: цепочка обязанностей)
Уровни изоляции транзакций
Read uncommited (грязное чтение) - самая менее затратная транзакция
Если одновременно запустить две транзакции. Внести изменения (insert, update, delete) в первой транзакции, вторая увидит изменения даже до того как первая транзакция их закомитит.Read commited
Вторая транзакция видит insert, update, delete сделанные в первой транзакции только после комита первой транзакции.Repeatable read
Вторая транзакция видит insert-ы закомиченные первой транзакцией, но не видит updat-ы и delet-ыSerializable - самая затратная, наименьший уровень параллелизма
Нельзя работать с данными прочитанными в другой транзакции,
Что еще спрашивают
IDisposable, try, catch, finally
Напишите singleton (не забудьте про потокобезопасность и lock)
домены приложений
синхронизации потоков (mutex, semaphore и т.п.)
Позитивные/негативные блокировки. Например: первый пользователь открыл форму на редактирование. Пока первый правит, второй успел внести изменения. Первый нажимает сохранить. Хорошо это или плохо? Какие варианты решения проблемы (если она есть)?
SQL запросы, особенно с HAVING
Самореклама
Делаю быстрый бесплатный редактор блок-схем и интеллект карт https://dgrm.net/
Литература
Stack and heap – .NET data structures
Boxing and Unboxing (C# Programming Guide)
Built-in reference types (C# reference)
Covariance and contravariance in generics
C# variance problem: Assigning List as List
Finalizers (C# Programming Guide)
Destructors in real world applications?
Virtual member call in a constructor
