Pull to refresh

Шпаргалка для собеседования .Net

Reading time9 min
Views103K

Ниже не учебник, а только шпаргалка для разработчиков уже знакомых с основами 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
boxing
// пример 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);
Программа после компиляции. Компилятор подставил значение const.
Программа после компиляции. Компилятор подставил значение const.

Фокус-покус с подкладыванием dll библиотеки, без перекомпиляции основного проекта:

Значение const в библиотеке отличается от используемого в основном проекте.
Значение const в библиотеке отличается от используемого в основном проекте.

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

Самореклама

Делаю быстрый бесплатный редактор блок-схем и интеллект карт 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

Наследование vs Композиция vs Агрегация

Fundamentals of garbage collection

Tags:
Hubs:
Total votes 17: ↑11 and ↓6+10
Comments0

Articles