CLR поддерживает (Family and Internal) - видимость только наследниками в сборке(и внутренними типами класса), в C# такого нет, есть только protected internal - видимость в своей сборке, видимость для наследников в любой сборке и видимость для внутренних типов, видимость для внутренних типов Кстати в nested классах доступны все закрытые члены внешнего класса:
public class Outer
{
private int Value;
public class Nested
{
private int NestedValue;
public Nested(Outer outer)
{
NestedValue = outer.Value - 12;
}
}
}
Статические классы используются в языке для классов не хранящих состояние, а только операции, например Math - класс для математических вычислений. Нельзя управлять жизнью этого объекта, он сам создается и уничтожается только по завершению приложения. В статическом классе нет экземлярного конструктора, при компиляции он сразу помечается как sealed, поэтому нельзя наследоваться от статического класса, а также abstract - чтобы его нельзя было создать. Статических класс - прямой наследник System.Object, наследование от другого базового класса лишено смысла, т.к нельзя создать класс наследник, нельзя реализовывать интерфейсы, потому что их можно вызывать только через экземпляры классов.Статические классы должны содержать только статические члены.Класс нельзя использовать как поле другого класса, потому что это в языке подразумевает существование экземпляра класса. Вызовы всех методов статического класса в IL пишутся как call, а не callvirt, поэтому никогда не могут приводить к NRE(только callvirt бросается NRE, если вызывается на null).
Можно разделить один класс на несколько файлов, а можно и писать все в одном файле, создав несколько определений одного класса с модификатором partial:
public partial class Class
{
}
public partial class Class
{
}
Такое может применяться если нужно отделить автосгенерированный код от самописного, автосгенерированный - генерируется IDE, а потом помещается в отдельных класс, о котором программист может и не знать, а реализовывает свой функционал он в таком же классе, который помечен как partial
Компонент - часть выделенного кода, выполняющего определенную цель, например, entity framework - компонент для моделирования домена вместе с базой данных, посколько компоненты развиваются, они имеют номера версий, в виде 1.2.3.4, где главные цифры стоят в начале 1 - major update, этот номер увеличивается, когда в компонент вносятся серьезные изменения и обратная совместимость(с младшими версиями) не гарантируется 2 - minor update, дополнительный номер версии, запилили еще несколько новых фич 3,4 - build и revision обычно связаны с баг фиксами
Методы бывает экземплярыми , виртуальными и статическими, при компиляции компилятор c# помещает три записи для каждого метода, которые определяют является ли методов виртуальным, экземплярным или статическим В clr есть две инструкции для вызова метода: call - можно вызвать любой метод, в том числе невиртуально вызвать виртуальный метод(тогда метод будет вызван в типе, на котором определен) Компилятор c# генерирует такую интсрукцию, когда вызывается статический метод или из переопределенного метода вызывается метод базового класса, во всех случаях, он понимает, что метод может вызвать этот метод невиртуально и вызывает его
callvirt - вызов виртуального метода, JIT генерирует NRE, если метод вызывается на null, callvirt - это более сложная операция, поскольку CLR должен понять реальный тип объекта на котором вызывается метод, например (метод Execute виртуальный, переопределен в классе B):
var a = new B(null, null);
a.Execute();
В данном случае IL код будет выглядеть так:
т.е в IL записывается вызов Execute базового класса, и нужно подняться по стеку и посмотреть какой реальный тип переменной a
Как я уже упомянал, при вызове базового метода генерируется инструкция call базового класса, но если у нас есть иерархия классов C : B : A, то вызов метода какого из классов должен быть записан, логично, если это будет вызов метода B, если вызываться из метода класса C:
public class B : A, IInterface
{
public override void Execute()
{
base.Execute();
}
}
public class D : B
{
public D(IInterface @interface, IEnumerable<int> i) : base(@interface, i)
{
}
public override void Execute()
{
Console.WriteLine("123");
base.Execute();
}
}
и правда записывается невиртуальный вызов метода определенного в классе B
А вот IL код, который генерируется для вызова Execute в классе B:
Вызывается базовый метод класса A, все логично
call - используется для вызова методов структур:
public struct Struct
{
public void Do()
{
Console.WriteLine("Do");
}
}
...
var s = new Struct();
s.Do();
Это делается потому что структура и так является sealed, т.е виртуальный метод просто не может быть вызван. Также для вызова виртуального метода нужно получить доступ к таблице методов, что можно сделать только получив ссылку на объект значимого типа, т.е упаковав его
Рихтер всегда по умолчанию делает классы sealed, это определенная оптимизация, поскольку JIT компилятор вызывает переопределенные методы таких классов невиртуально(хотя компилятор с# все-равно использует для вызова таких методов инструкцию callvirt):
public sealed class B : A, IInterface
{
public string GetName() => _interface.GetName();
public override void Execute()
{
Console.WriteLine("123");
}
}
Также он приводит аргумент, что если класс по умолчанию sealed, то его можно распечатать не нарушая совместимости, обратное же неверно, могут уже быть созданы классы, которые наследуются от типа, поэтому запечатать тип не всегда получится
И третий аргумент, что очень малая часть классов, которые мы используем для строительства иерархии, поэтому модификаторы класса, должны явно указывать его намерения, т.e делать sealed, если он нужен для наследования, программист может убрать модификатор sealed