Константами могут быть только примитивные типы, потому что они сохраняются в метаданные модуля, при компиляции, когда компилятор доходит до объявления константы в коже, он смотрит сборку в которой она определена, и извлекает ее значение из метаданных, таким образом в IL код подставляется значение константы Константы нельзя изменять, т.к они запоминаются один только один раз при компиляции.Константы не могут быть статическими,т.к и так принадлежат типу в котором определены(хотя по факту, они размещаются в метаданных модуля)
Поля - данные класса, они могут иметь разные модификаторы, static - принадлежит типу в котором определяется, Instance - принадлежит экземпляру, readonly - запись разрешается только из кода конструктора, volatile - запрещает компилятору оптимизировать код обращающийся к полю Для статических полей память выделяется в пределах типа, который создается при загрузке типа в домен приложения(при первом обращении JIT компилятора к методу, который обращается к типу), для экземплярных полей - во время создания экземпляра класса В СLR определены как поля как для чтения, так и для записи, инициализация readonly(в CLR initonly) полей разрешается только из конструктора
Readonly поля могут быть объявлены inline, в таком случае они инициализируются в статическом конструкторе при загрузке типа в домен
Плюс readonly полей над константами это то, что типы полей могут быть ссылочными, и значение полей может динамически изменяться если загрузить новую версию сборки в процессе выполнения приложения (у меня не получилось воспроизвести это, потому что при добавлении ссылки на сборку rider копирует dll в папку bin исполняемого проекта,а заменить на новую нельзя, т.к она занята в процессе), а значение констант "зашито" в IL код и недостаточно просто пересобрать сборку где объявлена константа, компилятору нужно сгенерировать новый IL код во всех местах использования константы