C # не дозволяє структурам отримувати з класів, але всі ValueTypes походять від Object. Де це розмежування?
Як CLR справляється з цим?
C # не дозволяє структурам отримувати з класів, але всі ValueTypes походять від Object. Де це розмежування?
Як CLR справляється з цим?
Відповіді:
C # не дозволяє структурам отримувати з класів
Ваше твердження неправильне, звідси і ваша плутанина. C # робить дозволяють витягати Структури з класів. Всі структури походять від одного класу System.ValueType, який походить від System.Object. І всі переліки походять від System.Enum.
ОНОВЛЕННЯ: У деяких (нині видалених) коментарях була певна плутанина, що вимагає роз'яснень. Я задам кілька додаткових питань:
Чи походять структури від базового типу?
Явно так. Ми можемо переконатися в цьому, прочитавши першу сторінку специфікації:
Усі типи C #, включаючи примітивні типи, такі як int та double, успадковуються від одного кореневого типу об'єкта.
Тепер я зауважу, що специфікація тут завищує випадок. Типи покажчиків не походять від об'єкта, і зв'язок виведення для типів інтерфейсу та типів параметрів типу є більш складним, ніж вказує цей ескіз. Однак очевидно, що це так, що всі типи структур походять від базового типу.
Чи існують інші способи, якими ми знаємо, що типи структур походять від базового типу?
Звичайно. Тип структури може замінити ToString
. Що це переважно, як не віртуальний метод свого базового типу? Тому він повинен мати базовий тип. Цей базовий тип - це клас.
Чи можу я вивести визначену користувачем структуру з вибраного мною класу?
Явно ні. Це не означає, що структури не походять від класу . Структури походять від класу і тим самим успадковують спадкових членів цього класу. Насправді, структури повинні походити з певного класу: Enum повинні походити з Enum
, структури повинні походити з ValueType
. Оскільки вони необхідні , мова C # забороняє вказувати відношення виведення у коді.
Навіщо це забороняти?
Коли потрібні відносини , конструктор мов має варіанти: (1) вимагати від користувача ввести необхідне заклинання, (2) зробити його необов’язковим або (3) заборонити. У кожного є плюси і мінуси, і дизайнери мови C # вибрали по-різному, залежно від конкретних деталей кожного.
Наприклад, поля const повинні бути статичними, але забороняється стверджувати, що вони є такими, оскільки це по-перше, безглуздо словосполучення, а по-друге, означає, що існують нестатичні поля const. Але перевантажені оператори повинні бути позначені як статичні, навіть якщо розробник не має вибору; занадто легко розробникам повірити, що перевантаження оператора - це метод екземпляра в іншому випадку. Це перекриває занепокоєння щодо того, що користувач може прийти до думки, що "статичність" означає, що, скажімо, "віртуальність" також є можливою.
У цьому випадку вимагати від користувача сказати, що їх структура походить від ValueType, здається простим надмірним багатослів’ям, і це означає, що структура може походити від іншого типу. Щоб усунути обидві ці проблеми, C # робить незаконним вказувати в коді, що структура походить від базового типу, хоча, очевидно, так і робить.
Подібно всі типи делегатів походять від MulticastDelegate
, але C # вимагає від вас не говорити цього.
Отже, зараз ми встановили, що всі структури в C # походять від класу .
Який взаємозв'язок між успадкуванням та виведенням з класу ?
Багатьох спантеличують відносини спадкування в C #. Відносини успадкування досить прості: якщо структура, клас або делегат типу D походить від класу типу B, то спадкові члени B також є членами D. Це так просто.
Що це означає щодо успадкування, коли ми говоримо, що структура походить від ValueType? Просто всі спадкові члени ValueType також є членами структури. Ось як структури отримують їх реалізацію ToString
, наприклад; він успадковується від базового класу структури.
Усі члени, що успадковуються? Звичайно, ні. Чи є приватні члени спадкоємними?
Так. Усі приватні члени базового класу також є членами похідного типу. Звичайно, забороняється називати цих учасників іменами, якщо сайт дзвінків не знаходиться в домені доступності учасника. Те, що у вас є учасник, не означає, що ви можете ним скористатися!
Тепер ми продовжуємо з оригінальною відповіддю:
Як CLR справляється з цим?
Надзвичайно добре. :-)
Що робить тип значення типом значення, це те, що його екземпляри копіюються за значенням . Що робить тип посилання типом посилання, це те, що його екземпляри копіюються за допомогою посилання . Ви, здається, певно переконані, що відносини успадкування між типами цінностей та референтними типами є якимось особливим і незвичним, але я не розумію, що це за переконання. Спадщина не має нічого спільного з тим, як речі копіюються.
Подивіться на це так. Припустимо, я сказав вам наступні факти:
Є два види коробок, червоні та сині.
Кожен червоний ящик порожній.
Є три спеціальні сині коробки, що називаються O, V та E.
O не знаходиться всередині жодної коробки.
V всередині O.
Е знаходиться всередині V.
Жодної іншої синьої коробки всередині V.
Жодної синьої коробки всередині E.
Кожне червоне поле знаходиться у V або E.
Кожна синя коробка, крім O, сама по собі знаходиться всередині синьої коробки.
Сині поля - це посилальні типи, червоні поля - типи значень, O - System.Object, V - System.ValueType, E - System.Enum, а взаємозв'язок "inside" - "походить від".
Це цілком послідовний і зрозумілий набір правил, який ви могли б легко реалізувати самі, якби у вас було багато картону і багато терпіння. Чи є коробка червоною чи синьою, не має нічого спільного з тим, що вона знаходиться всередині; в реальному світі цілком можливо поставити червону коробку всередині синьої коробки. У CLR абсолютно законно створити тип значення, який успадковується від посилального типу, якщо це або System.ValueType, або System.Enum.
Тож давайте переформулюємо ваше запитання:
Як ValueTypes походять від Object (ReferenceType) і все ще залишаються ValueTypes?
як
Як можливо, що кожне червоне поле (типи значень) знаходиться всередині (походить від) вікна O (System.Object), яке є синім полем (тип посилання) і при цьому залишається червоним полем (тип значення)?
Коли ви формулюєте це так, я сподіваюся, це очевидно. Ніщо не заважає вам помістити червону коробку всередину коробки V, яка знаходиться всередині коробки O, яка є синьою. Чому б там бути?
ДОДАТКОВЕ ОНОВЛЕННЯ:
Первісне запитання Джоан було про те, як це можливощо тип значення походить від посилального типу. Моя оригінальна відповідь насправді не пояснила жодного механізму, який CLR використовує для того, щоб врахувати той факт, що ми маємо відносини виведення між двома речами, які мають абсолютно різні уявлення - а саме: чи мають дані, на які посилаються, заголовок об’єкта, a блок синхронізації, чи володіє він власним сховищем для збирання сміття тощо. Ці механізми складні, занадто складні, щоб пояснити їх однією відповіддю. Правила системи типу CLR є набагато складнішими, ніж дещо спрощений її смак, який ми бачимо в C #, де немає чіткого розрізнення, наприклад, в коробковій та безкартонній версіях типу. Впровадження дженериків також призвело до додаткової складності, яка додалася до ЗРЗ.
Невелика корекція, C # не дозволяє структурам виходити з будь-чого, не лише з класів. Все, що може зробити структура, це реалізація інтерфейсу, який сильно відрізняється від виведення.
Я думаю, що найкращий спосіб відповісти на це ValueType
- особливий. По суті, це базовий клас для всіх типів значень у системі типів CLR. Важко знати, як відповісти "як CLR це робить", оскільки це просто правило CLR.
ValueType
це особливе, але варто прямо згадати, що ValueType
саме воно насправді є посилальним типом.
Це дещо штучна конструкція, яку підтримує CLR, щоб дозволити всі типи оброблятись як System.Object.
Типи значень походять від System.Object через System.ValueType , де відбувається спеціальна обробка (тобто: CLR обробляє бокси / розпакування тощо для будь-якого типу, що походить від ValueType).
Ваше твердження неправильне, звідси і ваша плутанина. C # дозволяє структурам виходити з класів. Всі структури походять від одного класу System.ValueType
Тож спробуймо це:
struct MyStruct : System.ValueType
{
}
Це навіть не буде скомпільовано. Компілятор нагадуватиме вам "Введіть" System.ValueType "у списку інтерфейсів не є інтерфейсом".
При декомпіляції Int32, яка є структурою, ви знайдете:
відкрита структура Int32: IComparable, IFormattable, IConvertible {}, не кажучи вже про те, що вона походить від System.ValueType. Але в браузері об’єктів ви виявляєте, що Int32 успадковує від System.ValueType.
Тож усе це змушує мене повірити:
Я думаю, що найкращий спосіб відповісти на це - це значення ValueType. По суті, це базовий клас для всіх типів значень у системі типів CLR. Важко знати, як відповісти "як CLR це робить", оскільки це просто правило CLR.
ValueType
, воно використовує це для визначення двох типів об'єктів: типу об'єкта купи, що поводиться як посилальний тип і тип місця зберігання, який фактично знаходиться поза системою успадкування типів. Оскільки ці два типи речей використовуються у взаємовиключних контекстах, дескриптори одного типу можна використовувати для обох. На рівні CLR структура визначається як клас, батьком якого є System.ValueType
, але C # ...
System.ValueType
), і забороняє класам вказувати, що вони успадковують, System.ValueType
оскільки будь-який клас, який був оголошений таким чином, поводився б як тип значення.
Тип значення в коробці є фактично еталонним типом (він ходить як один, а шаркає як один, настільки ефективно). Я б припустив, що ValueType насправді не є базовим типом типів значень, а є базовим типом посилання, до якого типи значень можуть бути перетворені при передачі в тип Object. Типи значень, що не містяться в коробці, самі знаходяться поза ієрархією об’єктів.
З усіх відповідей відповідь @ supercat наближається до фактичної відповіді. Оскільки інші відповіді насправді не відповідають на питання, і прямо кажуть неправильні твердження (наприклад, що типи значень успадковуються від чого завгодно), я вирішив відповісти на питання.
Ця відповідь базується на моєму власному зворотному проектуванні та специфікації CLI.
struct
і class
є ключовими словами C #. Що стосується CLI, то всі типи (класи, інтерфейси, структури тощо) визначаються визначеннями класів.
Наприклад, тип об'єкта (відомий в C # as class
) визначається наступним чином:
.class MyClass
{
}
Інтерфейс визначається визначенням класу із interface
семантичним атрибутом:
.class interface MyInterface
{
}
Причина, по якій структури можуть успадковувати System.ValueType
і все ще бути типами цінностей, полягає в тому, що .. вони цього не роблять.
Типи значень - це прості структури даних. Типи значень ні з чого не успадковуються і не можуть реалізувати інтерфейси. Типи значень не є підтипами будь-якого типу, і вони не мають жодної інформації про тип. Враховуючи адресу пам'яті типу значення, неможливо визначити, що представляє тип значення, на відміну від посилального типу, який містить інформацію про тип у прихованому полі.
Якщо уявити собі таку структуру C #:
namespace MyNamespace
{
struct MyValueType : ICloneable
{
public int A;
public int B;
public int C;
public object Clone()
{
// body omitted
}
}
}
Далі наведено визначення класу IL цієї структури:
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable
{
.field public int32 A;
.field public int32 B;
.field public int32 C;
.method public final hidebysig newslot virtual instance object Clone() cil managed
{
// body omitted
}
}
То що тут відбувається? Він чітко розширюється System.ValueType
, що є типом об'єкта / посилання, і реалізує System.ICloneable
.
Пояснення полягає в тому, що коли визначення класу розширюється, System.ValueType
воно насправді визначає 2 речі: тип значення та відповідний тип значення в коробці. Члени визначення класу визначають представлення як для типу значення, так і для відповідного типу в коробці. Це не тип значення, який розширює та реалізує, а відповідний тип у коробці. extends
і implements
ключових словах відносяться тільки до коробкового типу.
Для уточнення, визначення класу вище робить 2 речі:
System.ValueType
та реалізаціяSystem.ICloneable
інтерфейсу.Також зауважте, що будь-яке розширення визначення класу System.ValueType
також є внутрішньо герметичним, незалежно від того sealed
, вказано ключове слово чи ні.
Оскільки типи значень - це просто прості структури, не успадковуйте, не реалізовуйте і не підтримуйте поліморфізм, їх не можна використовувати з рештою системи типів. Щоб обійти це, поверх типу значення, CLR також визначає відповідний тип посилання з тими ж полями, відомий як коробковий тип. Отже, хоча тип значення не може бути переданий методам, що приймають an object
, його відповідний тип в коробці може .
Тепер, якби вам потрібно було визначити метод у C # like
public static void BlaBla(MyNamespace.MyValueType x)
,
Ви знаєте, що метод прийме тип значення MyNamespace.MyValueType
.
Вище ми дізналися, що визначення класу, яке є результатом struct
ключового слова в C #, насправді визначає як тип значення, так і тип об'єкта. Однак ми можемо посилатися лише на визначений тип значення. Незважаючи на те, що в специфікації CLI зазначено, що ключове слово constraint boxed
може використовуватися для посилання на коробку версії типу, це ключове слово не існує (див. ECMA-335, II.13.1 Типи посилань на значення). Але давайте уявимо, що це відбувається на мить.
Коли йдеться про типи в IL, підтримується кілька обмежень, серед яких є class
і valuetype
. Якщо ми використовуємо, valuetype MyNamespace.MyType
ми вказуємо визначення класу типу значення під назвою MyNamespace.MyType. Подібним чином ми можемо використовувати class MyNamespace.MyType
для визначення визначення класу типу об’єкта під назвою MyNamespace.MyType. Це означає, що в ІЛ ви можете мати тип значення (struct) та тип об’єкта (клас) з однаковим іменем і все одно розрізняти їх. Тепер, якби boxed
ключове слово, зазначене специфікацією CLI, насправді було реалізоване, ми змогли б його використовуватиboxed MyNamespace.MyType
для вказівки на тип типу значення значення класу визначення MyNamespace.MyType.
Отже, .method static void Print(valuetype MyNamespace.MyType test) cil managed
приймає тип значення, визначений визначенням класу типу значення з іменем MyNamespace.MyType
,
while .method static void Print(class MyNamespace.MyType test) cil managed
приймає тип об’єкта, визначений визначенням класу типу об’єкта з іменем MyNamespace.MyType
.
так само, якби boxed
було ключовим словом, .method static void Print(boxed MyNamespace.MyType test) cil managed
взяв би тип типу значення, визначений визначенням класу з іменем MyNamespace.MyType
.
Ви б тоді бути в змозі створити екземпляр упакованого типу , як і будь-який інший тип об'єкту і передавати його будь-який спосіб , який приймає System.ValueType
, object
або в boxed MyNamespace.MyValueType
якості аргументу, і це було б, для всіх намірів і цілей, робота , як і будь-якого іншого довідкового типу. Це НЕ тип значення, а відповідний тип типу значення в коробці.
Отже, підсумовуючи і відповісти на питання:
Типи значень не є посилальними типами і не успадковуються від System.ValueType
будь-якого іншого типу, і вони не можуть реалізовувати інтерфейси. Відповідні типи в коробці , які також визначені , успадковують інтерфейси System.ValueType
та можуть реалізовувати їх.
.class
Визначення визначає різні дії в залежності від обставин.
interface
вказано семантичний атрибут, визначення класу визначає інтерфейс.interface
семантичний атрибут не вказаний, і визначення не поширюється System.ValueType
, визначення класу визначає тип об'єкта (клас).interface
семантичний атрибут не вказаний, а визначення дійсно поширюється System.ValueType
, визначення класу визначає тип значення і його відповідний коробковий тип (структура).Цей розділ передбачає 32-розрядний процес
Як уже зазначалося, типи значень не мають інформації про тип, і, отже, неможливо визначити, що тип значення представляє за місцем його пам'яті. Структура описує простий тип даних і містить лише поля, які вона визначає:
public struct MyStruct
{
public int A;
public short B;
public int C;
}
Якщо ми уявимо, що екземпляр MyStruct був виділений за адресою 0x1000, то це макет пам'яті:
0x1000: int A;
0x1004: short B;
0x1006: 2 byte padding
0x1008: int C;
Структури за замовчуванням для послідовного макетування. Поля вирівнюються за межами власного розміру. Для цього додається прокладка.
Якщо ми визначимо клас точно так само, як:
public class MyClass
{
public int A;
public short B;
public int C;
}
Якщо уявити одну і ту ж адресу, макет пам'яті такий:
0x1000: Pointer to object header
0x1004: int A;
0x1008: int C;
0x100C: short B;
0x100E: 2 byte padding
0x1010: 4 bytes extra
Класи за замовчуванням мають автоматичне розміщення, і компілятор JIT розташує їх у найбільш оптимальному порядку. Поля вирівнюються за межами власного розміру. Для цього додається прокладка. Я не впевнений, чому, але в кінці кожного класу завжди є додаткові 4 байти.
Зсув 0 містить адресу заголовка об’єкта, який містить інформацію про тип, таблицю віртуальних методів тощо. Це дозволяє середовищу виконання ідентифікувати, що представляють дані за адресою, на відміну від типів значень.
Таким чином, типи значень не підтримують успадкування, інтерфейси та поліморфізм.
Типи значень не мають віртуальних таблиць методів і, отже, не підтримують поліморфізм. Однак їхній відповідний тип у коробці робить .
Коли ви маєте екземпляр структури і намагаєтесь викликати віртуальний метод, як ToString()
визначено далі System.Object
, час виконання повинен помітити структуру.
MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
Однак, якщо структура перевизначає, ToString()
тоді виклик буде статично прив'язаний, і час виконання буде викликати MyStruct.ToString()
без боксу та без перегляду будь-яких віртуальних таблиць методів (структури не мають таких). З цієї причини він також може вбудовувати ToString()
дзвінок.
Якщо структура перевизначає ToString()
та встановлена в поле, тоді виклик буде вирішено за допомогою таблиці віртуального методу.
System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct.
Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
Однак пам’ятайте, що ToString()
це визначено у структурі і, таким чином, оперує значенням структури, тому воно очікує тип значення. Тип коробки, як і будь-який інший клас, має заголовок об’єкта. Якби ToString()
метод, визначений у структурі, викликався безпосередньо з boxed-типом у this
покажчику, при спробі отримати доступ до поля A
в MyStruct
, він отримав би доступ до зміщення 0, яке в boxed-типі було б покажчиком заголовка об’єкта. Отже, тип коробки має прихований метод, який робить фактичне перевизначення ToString()
. Цей прихований метод розпаковує (лише обчислення адреси, як unbox
інструкція IL) тип boxed, після чого статично викликає ToString()
визначений у структурі.
Аналогічним чином, тип boxed має прихований метод для кожного реалізованого методу інтерфейсу, який робить те саме розпакування, а потім статично викликає метод, визначений у структурі.
I.8.2.4 Для кожного типу значення CTS визначає відповідний тип посилання, який називається коробковим типом. Зворотне не відповідає дійсності: загалом типи посилань не мають відповідного типу значення. Представлення значення типу boxed (boxed value) - це місце, де може зберігатися значення типу значення. Тип в коробці - це тип об’єкта, а значення в коробці - об’єкт.
I.8.9.7 Не всі типи, визначені визначенням класу, є об'єктними типами (див. §I.8.2.3); зокрема, типи значень не є об’єктними типами, але вони визначаються за допомогою визначення класу. Визначення класу для типу значення визначає як тип значення (без упаковки), так і асоційований тип у коробці (див. §I.8.2.4). Члени визначення класу визначають представлення обох.
II.10.1.3 Семантичні атрибути типу визначають, чи повинен бути визначений інтерфейс, клас чи тип значення. Атрибут interface визначає інтерфейс. Якщо цього атрибута немає і визначення поширюється (прямо чи опосередковано) System.ValueType, а визначення не стосується System.Enum, слід визначити тип значення (§II.13). В іншому випадку повинен бути визначений клас (§II.11).
I.8.9.10 У безкартонній формі типи значень не успадковуються від жодного типу. Типи значень, що вкладаються, повинні успадковувати безпосередньо від System.ValueType, якщо вони не є переліченнями, і в цьому випадку вони успадковуватимуть від System.Enum. Типи цінних паперів повинні бути скріплені печаткою.
II.13 Типи значень без упаковки не вважаються підтипами іншого типу, і неприпустимо використовувати інструкцію isinst (див. Розділ III) для типів значень без упаковки. Однак інструкція isinst може бути використана для типів значень, що містяться в коробці.
I.8.9.10 Тип значення не успадковує; швидше базовий тип, зазначений у визначенні класу, визначає базовий тип коробчатого типу.
I.8.9.7 Типи значень не підтримують інтерфейсні контракти, але пов'язані з ними типи, що містяться в коробці, підтримують.
II.13 Типи значень повинні реалізовувати нуль або більше інтерфейсів, але це має значення лише в коробковій формі (§II.13.3).
I.8.2.4 Інтерфейси та успадкування визначаються лише для посилальних типів. Таким чином, хоча визначення типу значення (§I.8.9.7) може вказувати як інтерфейси, які повинні бути реалізовані типом значення, так і клас (System.ValueType або System.Enum), від якого воно успадковується, вони стосуються лише значень, що містяться в коробці. .
II.13.1 Посилання на форму типу цінності без упаковки має бути використано за допомогою ключового слова valueetype, за яким слідує посилання на тип. Форма типу значення, що знаходиться в коробці, повинна посилатися, використовуючи ключове слово з коробкою, а потім посилання на тип.
Примітка: Тут специфікація помилкова, boxed
ключового слова немає .
Я думаю, що частина плутанини щодо того, як типи значень успадковуються, походить від того, що C # використовує синтаксис кастингу для виконання боксу та розпакування, через що здається, що ви виконуєте касти, що насправді не так (хоча, CLR викине InvalidCastException при спробі розпакувати неправильний тип).
(object)myStruct
у C # створює новий екземпляр коробчатого типу типу значення; він не виконує жодних приводів. Так само, (MyStruct)obj
в C # розпаковує коробковий тип, копіюючи частину значення; він не виконує жодних приводів.
System.ValueType
типу в системі типу CLR.