Шпаргалка для собеседования C# .Net 2023 год.
Ниже не учебник, а только шпаргалка для разработчиков уже знакомых с основами C# .Net.
Шпаргалка содержит только вопросы “на базу”. Вопросы вида “как бы вы спроектировали …”, “какие слои приложения …”, в шпаргалку не входят.Форматирование кода
В примерах, для краткости, открывающая скобочка { не на новой строке. Интервьюер может быть смущен, т.к. в C# принято ставить { с новой строки. Поэтому на собеседовании лучше использовать общепринятое форматирование.
@csharp_ci – лучший канал для C# .Net разработчиков
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> – указан out - Action<> контрвариантен -> общее можно привести к частному (пример ниже)
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
Литература
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
Наследование vs Композиция vs Агрегация
Fundamentals of garbage collection