Подтвердить что ты не робот

Подробная информация о том, что происходит, когда структура реализует интерфейс

Недавно я столкнулся с этим вопросом 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!?

В чем именно детальная семантика структуры типа значения обрабатывается как ссылочный тип?

4b9b3361

Ответ 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, так что все еще то, что у вас в этом поле для остальной части метода.