Подробная информация о том, что происходит, когда структура реализует интерфейс
Недавно я столкнулся с этим вопросом Stackoverflow: Когда использовать struct?
В нем у него был ответ, который сказал что-то немного глубокое:
Кроме того, поймите, что когда структура реализует интерфейс - как Перечислитель делает - и переносится на этот реализованный тип, структура становится ссылочным типом и перемещается в кучу. Внутренний Класс словаря, Enumerator все еще является типом значения. Однако, как только поскольку метод вызывает GetEnumerator(), IEnumerator ссылочного типа вернулся.
Что именно это означает?
Если бы у меня было что-то вроде
struct Foo : IFoo
{
public int Foobar;
}
class Bar
{
public IFoo Biz{get; set;} //assume this is Foo
}
...
var b=new Bar();
var f=b.Biz;
f.Foobar=123; //What would happen here
b.Biz.Foobar=567; //would this overwrite the above, or would it have no effect?
b.Biz=new Foo(); //and here!?
В чем именно детальная семантика структуры типа значения обрабатывается как ссылочный тип?
Ответы
Ответ 1
Каждое объявление типа структуры действительно объявляет два типа в Runtime: тип значения и тип объекта кучи. С точки зрения внешнего кода тип объекта кучи будет вести себя как класс с полями и методами соответствующего типа значений. С точки зрения внутреннего кода тип кучи будет вести себя так, как если бы у него было поле this
соответствующего типа значения.
Попытка присвоить тип значения ссылочному типу (Object
, ValueType
, Enum
или любой тип интерфейса) создаст новый экземпляр соответствующего типа объекта кучи и вернет ссылку на этот новый пример. То же самое произойдет, если попытаться сохранить тип значения в хранилище ссылочного типа или передать его как параметр ссылочного типа. Как только значение было преобразовано в объект кучи, оно будет вести себя - с точки зрения внешнего кода - как объект кучи.
Единственная ситуация, в которой реализация интерфейса типа значения может использоваться без типа значения, который сначала преобразуется в объект кучи, - это когда он передается как общий тип типа, который имеет тип интерфейса в качестве ограничения. В этой конкретной ситуации члены интерфейса могут использоваться в экземпляре типа значения без необходимости его преобразования в объект кучи.
Ответ 2
Прочитайте бокс и распаковать (выполните поиск в Интернете). Например, MSDN: Бокс и Unboxing (Руководство по программированию на С#).
См. также поток SO Почему нам нужны бокс и распаковка в С#? и потоки, связанные с этим потоком.
Примечание. Не так важно, если вы "конвертируете" в базовый класс типа значения, как в
object obj = new Foo(); // boxing
или "преобразовать" в реализованный интерфейс, как в
IFoo iFoo = new Foo(); // boxing
Только базовые классы a struct
имеют System.ValueType
и object
(включая dynamic
). Базовыми классами типа enum
являются System.Enum
, System.ValueType
и object
.
Структура может реализовать любое количество интерфейсов (но не наследует интерфейсов от базовых классов). Тип перечисления реализует IComparable
(не общий вариант), IFormattable
и IConvertible
, потому что базовый класс System.Enum
реализует эти три.
Ответ 3
Итак, я решил сам поместить это поведение на тест. Я дам "результаты", но я не могу объяснить, почему все происходит так. Надеюсь, кто-то, у кого больше знаний о том, как это работает, может прийти и рассказать мне более тщательный ответ
Полная тестовая программа:
using System;
namespace Test
{
interface IFoo
{
int Foobar{get;set;}
}
struct Foo : IFoo
{
public int Foobar{ get; set; }
}
class Bar
{
Foo tmp;
//public IFoo Biz{get;set;}; //behavior #1
public IFoo Biz{ get { return tmp; } set { tmp = (Foo) value; } } //behavior #2
public Bar()
{
Biz=new Foo(){Foobar=0};
}
}
class MainClass
{
public static void Main (string[] args)
{
var b=new Bar();
var f=b.Biz;
f.Foobar=123;
Console.WriteLine(f.Foobar); //123 in both
b.Biz.Foobar=567; /
Console.WriteLine(b.Biz.Foobar); //567 in behavior 1, 0 in 2
Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2
b.Biz=new Foo();
b.Biz.Foobar=5;
Console.WriteLine(b.Biz.Foobar); //5 in behavior 1, 0 in 2
Console.WriteLine(f.Foobar); //567 in behavior 1, 123 in 2
}
}
}
Как вы можете видеть, при ручном боксировании/распаковке мы получаем различное поведение чрезвычайно. Тем не менее, я не совсем понимаю поведение.
Ответ 4
Я отвечаю на ваш пост о вашем эксперименте в 2013-03-04, хотя я мог бы немного опоздать:)
Помните об этом: каждый раз, когда вы присваиваете значение struct переменной типа интерфейса (или возвращаете ее как тип интерфейса), она будет помещена в бокс. Подумайте об этом, как новый кусок (поле) будет создан в куче, а значение структуры будет скопировано. Это поле будет храниться до тех пор, пока у вас не будет ссылки на него, как на любой объект.
С поведением 1 у вас есть свойство auto auto типа IFoo, поэтому, когда вы устанавливаете здесь значение, оно будет помещено в бокс, и свойство сохранит ссылку на поле. Всякий раз, когда вы получаете значение свойства, поле будет возвращено. Таким образом, он в основном работает так, как если бы Foo был классом, и вы получаете то, что ожидаете: вы устанавливаете значение и получаете его обратно.
Теперь, с поведением 2, вы храните struct (field tmp), и ваше свойство Biz возвращает свое значение как IFoo. Это означает каждый раз, когда вызывается get_Biz, создается и возвращается новый ящик.
Посмотрите на главный метод: каждый раз, когда вы видите b.Biz, это другой объект (поле). Это объяснит фактическое поведение.
например. в строке
b.Biz.Foobar=567;
b.Biz возвращает поле в куче, вы устанавливаете Foobar в нем на 576, а затем, поскольку вы не держите ссылку на него, он немедленно теряется для вашей программы.
В следующей строке вы напишите b.Biz.Foobar, но этот вызов b.Biz затем снова создаст совершенно новый ящик с Foobar, имеющий значение по умолчанию 0, то, что напечатано.
Следующая строка, переменная f ранее была также заполнена вызовом b.Biz, который создал новое поле, но вы сохранили ссылку для этого (f) и установили его Foobar на 123, так что все еще то, что у вас в этом поле для остальной части метода.