Немного об интерфейсах в .Net (по мотивам одного интервью)

    В прошедний понедельник мне посчастливилось попасть на собеседование на Senior .Net Developer в одну международную компанию. Во время собеседования мне предложили пройти тест, где ряд вопросов был связан с .Net. В частности в одном из вопросов надо было дать оценку (истина/ложь) ряду утверждений, среди которых было и такое:

    В .Net любой массив элементов, например int[], по умолчанию реализует IList, что позволяет использовать его в качестве коллекции в операторе foreach.


    Быстро ответив на этот вопрос отрицательно и отдельно дописав на полях. что для foreach необходима реализация не IList, а IEnumerable, я перешел к следующему вопросу. Однако по дороге домой меня мучал вопрос: реализует ли массив все-таки этот интерфейс или нет?

    Про IList я смутно помнил, что этот интерфейс дает мне IEnumerable, индексатор и свойство Count, содержащее число элементов коллекции, а также еще пару редко используемых свойств, типа IsFixedCollection(). Массив имеет свойство Length для своего размера, а Count в IEnumerable является методом расширения от LINQ, что было бы невозможно, если бы этот метод был реализован в классе. Таким образом, получалось, что массив не мог реализовывать интерфейс IList, однако какое-то смутное чувство не давало мне покоя. Поэтому вечером после интервью я решил провести небольшое исследование.


    Класс System.Array


    Поскольку Reflector.Net у меня не был установлен, я просто написал короткую программку на С# чтобы узнать, что за интерфейсы реализуются целочисленным массивом.

    var v = new int[] { 1, 2, 3 };
    var t = v.GetType();
    var i = t.GetInterfaces();
    foreach(var tp in i)
         Console.WriteLine(tp.Name);
    


    Вот полный список полученных интерфейсов из окна консоли:

    ICloneable
    IList
    ICollection
    IEnumerable
    IStructuralComparable
    IStructuralEquatable
    IList`1
    ICollection`1
    IEnumerable`1
    IReadOnlyList`1
    IReadOnlyCollection`1
    


    Таким образом, массив в .Net все-таки реализует интерфейс IList и его обобщённый вариант IList<>.

    Чтобы получить более полную информацию я построил диаграмму класса System.Array.



    Мне сразу бросилась в глаза моя ошибка: Count было свойством не IList, а ICollection, еще предыдущего интерфейса в цепочке наследования. Тем не менее, сам массив уже не имел такого свойства, как и многих других свойств интерфейса IList, хотя другие свойства этого интерфейса, IsFixedSize и IsReadOnly были реализованы. Как такое вообще возможно?

    Всё сразу встает на свои места, когда вспоминаешь о том, что в С# можно реализовывать интерфейсы не только
    неявно (implicit), но и явно (explicit). Я знал об этой возможности из учебников, где приводился пример такой имплементации в случае. когда базовый класс уже содержит метод с тем же именем, что и метод интерфейса. Я также видел такую возможность в ReSharper. Однако до настоящего времени мне напрямую не приходилось сталкиваться с необходимостью явной реализации интерфейсов в моих собственных проектах.

    Сравнение явной и неявной реализации интерфейсов


    Давайте сравним эти два вида реализации интерфейсов:.

    Критерии
    Неявная (implicit) реализация
    Явная (explicit) реализация
    Базовый синтаксис
    interface ITest
    {
        void DoTest();
    }
    public class ImplicitTest : ITest
    {
        public void DoTest()
        { }
    }
    
    

    interface ITest
    {
        void DoTest();
    }
    public class ExplicitTest : ITest
    {
        void ITest.DoTest()
        { }
    }
    
    


    Видимость
    Неявная имплементация всегда являелся открытой (public), поэтому к методам и свойствам можно обращаться напрямую.
    var imp = new ImplicitTest();
    imp.DoTest();
    

    Явная имплементация всегда закрыта (private).
    Чтобы получить доступ к имплементации необходимо кастовать инстанцию класса к интерфейсу (upcast to interface).
    var exp = new ExplicitTest();
    ((ITest)exp).DoTest();
    

    Полиморфия
    Неявная имплементация интерфейса может быть виртуальной (virtual), что позволяет переписывать эту имплементацию в классах-потомках.
    Явная имплементация всегда статична. Она не может быть переписана (override) или перекрыта (new) в классах-потомках. Прим. 1
    Абстрактный класс и реализация
    Неявная реализация может быть абстрактной и реализовываться только в классе-потомке.
    Явная реализация не может быть абстрактной, но сам класс может иметь другие абстрактные методы и сам быть абстрактным.Прим. 2

    Примечания:
    Прим. 1 — Как справедливо замечает mayorovp в комментариях, реализация может быть переопределена при повторной явной имплементации интерфейса в классе-потомке (см. первый комментарий к статье).

    Прим. 2 — В одном из блогов указано, что класс сам не может быть абстрактным. Возможно это было верно для какой-то из предыдущих версий компилятора, в моих экспериментах я без проблем мог реализовать интерфейс явно в абстрактном классе.

    Зачем нужна явная реализация интерфейсов


    Явная реализация интерфейса, согласно MSDN, необходима в том случае, когда несколько интерфейсов, реализуемых классом, имеют метод с одинаковой сигнатурой. Эта проблема в общем виде известна в англоязычном мире под леденящим кровь названием «deadly diamond of death», что переводится на русский как «проблема ромба». Вот пример такой ситуации:

    /*   Listing 1   */
    interface IJogger
    {
      void Run();
    }
    
    interface ISkier
    {
      void Run();
    }
    
    public class Athlete: ISkier, IJogger
    {
    	public void Run() 
    	{
    	   Console.WriteLine("Am I an Athlete, Skier or Jogger?");
    	}
    }
    


    Кстати, этот пример является корректным кодом в C#, то есть он (корректно) компилируется и запускается, при этом метод Run() является одновременно и методом самого класса, и реализацией аж двух интерфейсов. Таким образом, мы можем иметь одну реализацию для разных интерфейсов и для самого класса. Проверить это можно следующим кодом:

    /*   Listing 2   */
    var sp = new Athlete();
    sp.Run();
    (sp as ISkier).Run();
    (sp as IJogger).Run();
    


    Результатом исполнения этого кода будет «Am I an Athlete, Skier or Jogger?», выведенное в консоли три раза.

    Именно здесь мы можем использовать явную реализацию интерфейса для того, чтобы разделить все три случая:

    /*   Listing 3   */
    public class Sportsman
    {
    	public virtual void Run()
    	{
    		Console.WriteLine("I am a Sportsman");
    	}
    }
    
    public class Athlete: Sportsman, ISkier, IJogger
    {
    	public override void Run() 
    	{
    	   Console.WriteLine("I am an Athlete");
    	}
    	
    	void ISkier.Run() 
    	{
    	   Console.WriteLine("I am a Skier");
    	}
    	
    	void IJogger.Run() 
    	{
    	   Console.WriteLine("I am a Jogger");
    	}
    }
    
    


    В данном случае при исполнении кода из Listing 2 мы увидим в консоли три строчки, «I am an Athlete», «I am a Skier» и «I am a Jogger».

    Плюсы и минусы различной реализации интерфейсов


    Видимость реализации и выборочная реализация

    Как уже было показано выше, неявная (implicit) реализация синтаксически не отличается от обычного метода класса (причём если этот метод уже был определен в классе-предке, то в таком синтаксисе метод будет сокрыт (hidden) в потомке и код будет без проблем скомпилирован c compiler warning о сокрытии метода.). Более того, возможна выборочная реализация отдельных методов одного интерфейса как явным, так и неявным образом:

    /* Listing 4 */
    public class Code
    {
      public void Run() 
      {
      	Console.WriteLine("I am a class method");
      }
    }
    
    interface ICommand
    {
      void Run();
      void Execute();
    }
    
    public class CodeCommand : Code, ICommand
    {
      // implicit interface method implementation
      //  => public implementation
      // implicit base class method hiding (warning here)
      public void Run() 
      {
    	base.Run();
      }
      
      // explicit interface method implementation
      //  => private implementation
      void ICommand.Execute()
      {}
    }
    


    Это позволяет использовать реализации отдельных методов интерфейса как родных методов класса и они доступны, например, через IntelliSense, в отличие от явной реализации методов, которые являются приватными и видны только после каста к соответствующему интерфейсу.

    С другой стороны, возможность приватной реализации методов позволяет скрывать ряд методов интерфейса, при этом полностью его имплементируя. Возвращаясь к нашему самому первому примеру с массивами в .Net, можно увидеть, что массив скрывает, например, имплементацию свойства Count интерфейса ICollection, выставляя наружу это свойство под именем Length (вероятно это является попыткой поддержания совместимости с С++ STL и Java). Таким образом, мы можем скрывать отдельные методы реализованного интерфейса и не скрывать (=делать публичными) другие.

    Здесь, правда, возникает такая проблема, что во многих случаях совершенно невозможно догадаться о том, какие интерфейсы реализованы классом «неявно», поскольку ни методы, ни свойства этих интерфейсов не видны в IntelliSense (здесь также показателен пример с System.Array). Единственным способом выявления таких реализаций является использование рефлексии, например при помощи Object Browser в Visual Studio.

    Рефакторинг интерфейсов

    Так как неявная (публичная) имплементация интерфейса не отличается от реализации публичного метода класса, в случае рефакторинга интерфейса и удаления из него какого-либо публичного метода (например при объединении методов Run() и Execute() из вышепредставленного интерфейса ICommand в один метод Run()) во всех неявных реализациях останется метод с открытым доступом, который, очень вероятно, придётся поддерживать даже после рефакторинга, так как у данного публичного метода могут быть уже различные зависимости в других компонентах системы. В результате этого будет нарушаться принцип программирования «против интерфейсов, а не реализаций», так как зависимости будут уже между конкретными (и в разных классах, наверняка, разными) реализациями бывшего интерфейсного метода.

    /* Listing 5 */
    interface IFingers
    {
      void Thumb();
      void IndexFinger();
      // an obsolete interface method
      // void MiddleFinger();  
    }
    
    public class HumanPalm : IFingers
    {
      public void Thumb() {}
      public void IndexFinger() {}
      // here is a "dangling" public method
      public void MiddleFinger() {}
    }
    
    public class AntropoidHand : IFingers
    {
       void IFingers.Thumb() {}
       void IFingers.IndexFinger() {}
       // here the compiler error
       void IFingers.MiddleFinger() {}
    }
    
    


    В случае приватной реализации интерфейсов все классы с явной реализацией несуществующего более метода просто перестанут компилироваться, однако после удаления ставшей ненужной реализации (или ее рефакторинга в новый метод) у нас не будет «лишнего» публичного метода, не привязанного к какому-либо интерфейсу. Конечно, возможно потребуется рефакторинг зависимостей от самого интерфейса, но здесь, по крайней мере, не будет нарушения принципа «program to interfaces, not implementations».

    Что касается свойств, то неявно реализованные свойства интерфейса (properties) позволяют обращаться к ним через методы-акцесоры (getter и setter) как извне, так и непосредственно из самого класса, что может привести к ненужным эффектам (например, к ненужной валидации данных при инициализации свойств).

    /* Listing 6 */
    interface IProperty
    {
      int Amount { get; set; }
    }
    
    public class ClassWithProperty : IProperty
    {
        // implicit implementation, public
    	public int Amount { get; set; }
    	public ClassWithProperty()
    	{
    	    // internal invocation of the public setter
    		Amount = 1000;
    	}
    }
    
    public class ClassWithExplicitProperty : IProperty
    {
          // explicit implementation, private
    	int IProperty.Amount { get; set; }
    	public ClassWithExplicitProperty()
    	{
    	    // internal invocation isn't possible
    	     // compiler error here
    	    Amount = 1000;
    	}
    }
    
    


    При явной имплементации свойств интерфейса эти свойства остаются приватными и для доступа приходится идти «длинным» путём и объявлять дополнительную закрытое поле, через которое и происходит инициализация. В результате это приводит к более чистому коду, когда методы доступа к свойству используются только для доступа извне.

    Использования явной типизации локальных переменных и полей классов

    В случае явной реализации интерфейсов нам приходится явным образом указывать, что мы работаем не экземпляром класса, а с экземпляром интерфейса. Таким образом, например, становится невозможным использование type inference и декларация локальных переменных в С# при помощи служебного слова var. Вместо этого нам приходится использовать явную декларацию с указанием типа интерфейса при объявлении локальных переменных, а также в сигнатуре методов и в полях класса.

    Таким образом, мы с одной стороны как бы делаем код несколько менее гибким (например ReSharper по умолчанию всегда предлагает использовать декларацию с var если это возможно), но зато избегаем потенциальных проблем, связанных с привязкой к конкретной имплементации, по мере роста системы и объема её кода. Этот пункт может показаться многим спорным, но в случае, когда над проектом работает несколько человек, да еще в разных концах света, использования явной типизации может быть очень даже полезным, так как это повышает читабельность кода и уменьшает затраты на его поддержку.

    Источники по теме
    При подготовке статьи использовалась информация из ряда сетевых источников, в частности из блогов ( [1], [2], [3] и [4]), а также из [5] и [6] вопросов со StackOverflow, очень интересной статьи на CodeProject и главы 13.5 книги Jeffrey Richter "CLR via C#".
    Небольшой бонус: два вопроса на засыпку (для пытливых)
    Эти вопросы не имеют прямого отношения к теме явной имплементации интерфейсов, но мне кажется, что здесь они могут быть кому-то интересны:
    1. Если к Listing 2 приписать еще одну строчку
    (sp as Sportsman).Run();
    

    То что будет выведено в консоль?

    2. Как при помощи минимального изменения в Listing 3 (замены одного ключевого слова другим) добиться вывода в консоль фразы «I am a Sportsman» в первом вопросе?

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 91

      +32
      Три замечания.

      Замечание первое.
      Явная имплементация всегда статична. Она не может быть переписана (override) или перекрыта (new) в классах-потомках.
      Не вполне верно. Явная имплементация по сути может быть переписана в классе-потомке, если тот же самый интерфейс реализовать еще раз. При этом используется другой механизм, нежели override, но результат аналогичен.
      interface A { void Foo(); }
      class B : A { void A.Foo() {} } // Явная реализация
      class C : B, A { void A.Foo() {} } // Аналог overide в виде еще одной явной реализации
      


      Замечание второе.
      Явная имплементация всегда приватна (private).
      В C# — да, явная реализация всегда приватна (точнее, она всегда private virtual sealed). Но статья все-таки называется «Немного об интерфейсах в .Net» — а в других языках этого семейства это не так. Я говорю о MSIL и C++/CLI. В этих языках при реализации интерфейса реализующий метод может иметь любой модификатор доступа и имя:
      public interface class A { 
      	void Foo(); 
      };
      
      public ref class B : A {
      protected:
      	virtual void Bar() = A::Foo {}
      };
      

      Кстати, это — еще один контрпример к первому замечанию.

      Замечание третье:
      для foreach необходима имплементация не IList, а IEnumerable
      Неверно. Для foreach необходимо наличие открытого метода GetEnumerator в классе или в одном из интерфейсов, возвращающего объект или структуру, у которого есть открытые метод bool MoveNext() и доступное для чтения свойство Current. Этот механизм позволяет в некоторых случаях (например, при итерации списков) создавать итератор не в куче, а на стеке.
          struct EmptyEnumerable
          {
              public EmptyEnumerator GetEnumerator() { return new EmptyEnumerator(); }
          }
      
          struct EmptyEnumerator
          {
              public bool MoveNext() { return false; }
              public object Current { get { throw new InvalidOperationException(); } }
          }
      
          class Test
          {
              public void Run()
              {
                  foreach (var q in new EmptyEnumerable())
                      throw new InvalidOperationException();
                  Console.WriteLine("Hello, world!");
              }
          }
      
        +1
        Для foreach необходимо наличие открытого метода GetEnumerator в классе или в одном из интерфейсов, возвращающего объект или структуру, у которого есть открытые метод bool MoveNext() и доступное для чтения свойство Current. Этот механизм позволяет в некоторых случаях (например, при итерации списков) создавать итератор не в куче, а на стеке.

        т.е. при использовании foreach происходит не приведение объекта к интерфейсу IEnumerable, а поиск метода GetEnumerator с помощью рефлексии?
          +10
          Нет. Происходит поиск метода GetEnumerator на этапе компиляции.
          Попробуйте скомпилировать тот код, который я привел, а потом декомпилировать в ildasm, если не понятно что происходит.
            –1
            Поддержу человека. Зря минусов ему наставили, потому что игнорирование наследования от интерфейса действительно вводит в ступор. А на счет создания итератора в стеке, так никто наследования структур от интерфейсов не отменял. Приведенный ниже пример работает отлично. Ну а то, что он без интерфейса может работать, так это, как по мне, большой недочет.

            public struct Iterator: IEnumerable
            {
                public IEnumerator GetEnumerator()
                {
                    for (int i = 0; i < 10; i++)
                    {
                        yield return i;
                    }
                }
            }
              0
              обалдеть, еще и заминусовать кто-то успел)я уже отвык от того, что тут минусы лепят просто так
                0
                Вот разбор 4х вариантов генерации кода для ForEach
                  +2
                  Интерфейсы всегда имеют ссылочный тип. Поэтому в данном случае реализовывать интерфейс в структуре имеет мало смысла. Потому что если бы foreach умел работать только интерфейсом, то все равно пришлось бы осуществлять операцию упаковки для этой самой структуры. И не существовало бы способа обойтись без упаковки.
                +2
                Спасибо за очень интересные замечания!
                0
                По форме — англицизмы раздражают, да ещё и приправленные тавтологией — имплементирован/имплементирует/имплементация, генерический вариант и т.п.

                По сути — интересное исследование.
                  +1
                  Спасибо за замечание и за оценку!

                  Я, к сожалению, очень редко читаю техническую литературу на русском языке и мне сложно подбирать термины. Как было бы Вы написали «имплементация» по-русски? И разве по-русски нет выражения «генерические классы»? Я постараюсь исправить статью, если действительно здесь есть хороший русский термин.
                    +1
                    Имплементация это почти всегда реализация.
                      +2
                      Спасибо!
                      Если у Вас есть еще какие-то предложения по улучшению русского языка, я с радостью постараюсь и их учесть.
                      +2
                      Устоявшиеся русские термины:
                      «Реализация [интерфейса]»
                      «Обобщенные классы»
                        0
                        О, спасибо, поправлю статью когда доберусь до работы!
                          0
                          «Обобщённые» — не слышал.
                          «Шаблонные»!
                            0
                            Я уже называл причину, по которой «шаблонные» — не лучший перевод
                              –3
                              Managed C++ это извращение, которое обычно служит для связки модулей, а не является главным языком проекта, где обычно пишут сложные классы. Вот и наблюдаем коллизии реализаций одного и того же подхода, пришедших из разных миров. Но ведь обе эти реализации по сути предназначены для решения одной задачи, поэтому приём, независимо от реализации, можно обозвать одним словом (с уточнением в редких случаях, когда есть опасность перепутать).
                          +4
                          >Как было бы Вы написали «имплементация» по-русски?

                          На мой взгляд вполне нормальное слово, но есть еще «реализация»

                          >И разве по-русски нет выражения «генерические классы»?
                          Такого вообще никогда не слышал :) Или «дженерики» или «обобщения»

                          — P.S. Я буду обновлять страницу перед отсылкой комментария
                            0
                            Дженерики? Вы уверены? Для меня это слово совсем не звучит по-русски…
                              +1
                              А это и не по-русски, просто ближе к названию на английском)
                            +2
                            имплементация — реализация
                            генерические — обобщенные
                            инстанция (это вообще ужас) — экземпляр

                            Я тоже редко читаю на русском (и использую слово имплементация в устной речи), но если пишешь для русской аудитории, лучше писать корректным языком, как мне кажется.

                            UPD: не обновил страницу, быстро написали. «Дженерики» тоже использую в устной речи, английские слова всегда почти лучше кальки с них
                              0
                              Да, но у меня проблема заключается в том, что я никогда не общаюсь с русской аудиторией.
                              Я начал изучать информатику в Германии и здесь же закончил университет, поэтому я знаю все термины либо как они были в книгах (на английском), либо как их использовали в своей речи немецкие профессора. Поэтому для меня Implementation, Instanz, Generische Klassen — вполне нормальные слова и выражения. Поэтому я охотно исправляю на более принятые в русском языке термины.
                                0
                                Я тоже редко читаю на русском (и использую слово имплементация в устной речи), но если пишешь для русской аудитории, лучше писать корректным языком, как мне кажется.

                                Настолько привык к английским терминам, что на русском часто путаюсь. Например, переопределение/перекрытие/перегрузка/сокрытие — что из этого override, что new?
                                  0
                                  Я бы сказал переопределение — override, перегрузка — overload, перекрытие — та же сигнатура без new, сокрытие — та же сигнатура с new. Последние два, впрочем, сложно различить. Но это скорее из-за того, что обычно new не используется, т.к. плохой стиль и скорее как редкий воркэраунд используется.
                                0
                                >>И разве по-русски нет выражения «генерические классы»
                                В русских текстах часто пишут «Generic-классы». Термин «обобщённые классы» не все сходу воспринимают, а слово «генерические» действительно режет глаз. Поэтому разумно оставить соответствующий термин без перевода — и так все поймут что имеется ввиду.
                                  0
                                  Имплементация = реализация, например.
                                  По-русски «генерические»? Типичный англицизм от «generic», варианты перевода на данный момент — шаблонный класс, класс-шаблон, обобщенный класс; интересная дискуссия на эту тему есть здесь, в последнем посте (от 2004 г.) есть и ваш вариант в качестве… шутки =)
                                  Microsoft использует перевод «универсальный класс», тоже вполне годный вариант.
                                    +1
                                    Вариант «шаблонный класс» лучше не использовать, поскольку в C++/CLI существуют как шаблонные (template), так и обобщенные (generic) классы.
                                      0
                                      Пожалуй, правильнее всего использовать вариант Microsoft, хотя бы для того, чтобы тебя понимали те, кто читает MSDN на русском. Для тех, кто предпочитает оригинальный MSDN, в скобках указывать оригинальное выражение. В идеале же, после написания статьи, неплохо бы сделать список/словарь многократно использованных переведенных терминов и поместить в начало статьи. Что типа такого —

                                      Универсальный класс — generic class;
                                      Уиниверсальный тип — generic type;


                                      Наверное, такой подход был бы максимально удобен читателю.
                                        0
                                        Лично мне бы во время чтения мешали лишние упоминания в скобках. А список терминов в конце статьи помог бы мало — я же её уже прочел. Прыгать туда-сюда во время чтения не очень хорошо
                                          0
                                          В комментарии написано — и поместить в начало статьи.
                                            0
                                            проглядел, сорри
                                            +1
                                            Пожалуй, правильнее всего использовать вариант Microsoft

                                            Мелкомягких за их автоматические переводы иногда хочется взять и у**ать. Они такие хитровы**анные термины выдумывают, что в жизни не догадаешься, что имелось в виду. Чего стоит перевод attached properties, например… Если в статье будут употребляться мелкомягкие «термины», то я её читать смогу с большим трудом.
                                      0
                                      Меня тоже немного покоробило.
                                      У нас для интерфейсов используется «Реализация», для Generic — «Обобщенные» или «Типизированные».

                                      P.S.:
                                      К нам пришел новый тестировщик, и каждый раз когда он говорит что-то вроде «Бага репродьюсится» — так и хочется запустить в него чем-нибудь тяжелым.
                                        0
                                        Давайте все же не будем придираться к словам. Ну написал человек как написал, все же прекрасно поняли смысл текста. Между прочим, тут часто встречаются фразы в стиле: «Полазив по сайтам, появилась идея...». Это в принципе неграмотно, т.к. отсутствует связь между частями предложения. Правильный вариант «Полазив по сайтам, мы придумали...». Ну и т.п.
                                          0
                                          Спасибо за тёплые слова!
                                          Поскольку у меня с русским языком, как у многих «технарей», еще со школы было все просто ужасно, я рад что по крайней мере меня понимают и буду стараться расширять свой словарный запас и русской технической лексикой.
                                            0
                                            Ну здесь же речь о терминологии, а не о русском языке. И на специализированном сайте это весьма актуальная проблема.
                                            Ничего личного против автора не имею, сама статья дала пищу для размышлений.
                                        +1
                                        Спасибо за статью — хотя я бы сделал ещё упор немного на другое:
                                        Для меня тоже было откровением, что array реализует интерфейс IList. Ведь IList — это в первую очередь методы Add() и Remove(). Поэтому логично было ответить, что array не реализует интерфейс IList.
                                        Однако чуда не произошло: хоть массив и реализует этот интерфейс — все методы списка (Add(), Clear(), Remove(), RemoveAt(), Insert()) им не поддерживаются.
                                        Так что не понятно, зачем такая реализация вообще нужна. Если я передаю в метод параметр типа IList<>, то очевидно, что я хочу в этом методе использовать методы специфичные именно для этого интерфейса (в противном случае я бы использовал параметр IEnumerable<>). А так получается есть возможность без проверки передать массив в качестве такого параметра, чтобы потом в рантайме вылезла ошибка NotSupportedException.
                                          +4
                                          IList — это в первую очередь доступ к элементам по индексу, и массив его поддерживает.

                                          А те методы, которые вы перечислили — это методы списка переменного размера, которым массив не является.

                                          Загляните в документацию (в msdn, например):
                                          Представляет коллекцию объектов, доступ к которым может быть получен отдельно, по индексу.
                                          Где здесь вообще сказано про изменяемость? Все изменяющие методы — опциональны.
                                            +1
                                            Да, Вы не первый, у кого возникает такой вопрос — у меня он тоже возник в процессе моего исследования.
                                            Лучше всего на него, по-моему, отвечает John Skeet в одном из своих ответов на StackOverflow. Также мне интересным показался и вот этот вопрос, и особенно статья, на которую там дана ссылка.
                                              +2
                                              Советую почитать C# in Depth за авторством Джона Скита. Там очень многие вопросы из приведенных в статье\комментариях рассматриваются и ваш конкретно расписан очень подробно.
                                                0
                                                Спасибо!
                                                Я начинал читать её, но так и не осилил до конца. Мне очень нравится его стиль изложения, когда один и то же код постепенно улучшается с каждой версией C#.
                                              –2
                                              Вообще, конечно, уровень учебника по C# средней руки.
                                                +3
                                                Спасибо, буду считать это комплиментом и надеяться дорасти когда-нибудь до таких корифеев, как Рихтер или Трёлсен. :)
                                                  0
                                                  Желаю удачи в профессиональном росте! Только имейте ввиду, что «Troelsen» в русском варианте пишется как «Троелсен».
                                                    0
                                                    Хм… может быть это американское прочтение его фамилии?
                                                    Спасибо за замечание!
                                                    +1
                                                    Советую задумался о заучивании книги «CLR via C#» Джеффри Рихтера. Там все эти вопросы и еще много много других освещены подробно.
                                                      0
                                                      Если бы Рихтер писал стихами, то я бы с удовольствием их заучил :)
                                                      А так мне каждый раз приходится заново перечитывать, потому что к середине книге я уже напрочь забыл все эти особенности поиска и подгрузки assembly, что он рассматривает в первой и второй главе…
                                                  +1
                                                  Вообще mayorovp все уже отметил, но вот обычно в реализациях компилятора, когда он встречает forach и мы имеем массив, то там идет оптимизация и на выходе мы будем иметь for (var i = 0; i < array.Length; i++). Такая оптимизация есть и в Mono и в Microsoft .NET. Поэтому, в большинстве случаев, вызова GetEnumerator у массива не будет.
                                                    0
                                                    Можно подкрепить источниками?
                                                      0
                                                      В книге C# in Depth, которую я упоминал выше, так написано, считаю надежным источником. К тому же какие проблемы скомпилить foreach для int[] и List<int> и посмотреть дизассемблированный код? Я проверил — в IL для первого случая вызовов GetEnumerator и проч. нету, для второго — есть.
                                                        0
                                                        Да я-то скомпилировал, и знаю это. Просто нужен какой-нибудь компетентный источник, чтоб тыкать людей туда носом.
                                                          0
                                                          Вот этот не подойдёт?
                                                        0
                                                        А тут не нужны источнки, скачайте моно. И скомпилируйте 2 цикла с foreach и for, и посмотрите IL код. Тело цикла будет совпадать в обоих случаях (также и для Microsoft.NET). Конечно в блоге разработчиков где-то есть описание но искать лень.
                                                          +1
                                                          Допустим у нас есть следующий код:

                                                          using System;
                                                          using System.Linq;
                                                          
                                                          namespace TestArrayInForeach
                                                          {
                                                              class Program
                                                              {
                                                                  static void Main(string[] args)
                                                                  {
                                                                      var array = args.Select(int.Parse).ToArray();
                                                                      var arrayLength = array.Length;
                                                          
                                                                      // First case
                                                                      for (var index = 0; index < array.Length; index++)
                                                                      {
                                                                          Console.WriteLine(array[index]);
                                                                      }
                                                          
                                                                      // Second case
                                                                      foreach (int item in array)
                                                                      {
                                                                          Console.WriteLine(item);
                                                                      }
                                                          
                                                                      // Third case
                                                                      for (var index = 0; index < arrayLength; index++)
                                                                      {
                                                                          Console.WriteLine(array[index]);
                                                                      }
                                                                  }
                                                              }
                                                          }
                                                          


                                                          Скомпилируем его:

                                                          Первый цикл:
                                                                      {
                                                                          Console.WriteLine(array[index]);
                                                          00000092  mov         eax,dword ptr [ebp-8] 
                                                          00000095  mov         edx,dword ptr [ebp-1Ch] 
                                                          00000098  cmp         eax,dword ptr [edx+4] 
                                                          0000009b  jb          000000A2 
                                                          0000009d  call        79033254 
                                                          000000a2  mov         ecx,dword ptr [edx+eax*4+8] 
                                                          000000a6  call        048F4D90 
                                                                      for (var index = 0; index < array.Length; index++)
                                                          000000ab  inc         dword ptr [ebp-8] 
                                                          000000ae  mov         eax,dword ptr [ebp-8] 
                                                          000000b1  mov         edx,dword ptr [ebp-1Ch] 
                                                          000000b4  cmp         eax,dword ptr [edx+4] 
                                                          000000b7  jl          00000092 
                                                          


                                                          Второй цикл:
                                                          000000c7  mov         eax,dword ptr [ebp-14h] 
                                                          000000ca  mov         edx,dword ptr [ebp-20h] 
                                                          000000cd  cmp         eax,dword ptr [edx+4] 
                                                          000000d0  jb          000000D7 
                                                          000000d2  call        79033254 
                                                          000000d7  mov         eax,dword ptr [edx+eax*4+8] 
                                                          000000db  mov         dword ptr [ebp-0Ch],eax 
                                                                      {
                                                                          Console.WriteLine(item);
                                                          000000de  mov         ecx,dword ptr [ebp-0Ch] 
                                                          000000e1  call        048F4D90 
                                                          000000e6  inc         dword ptr [ebp-14h] 
                                                                      foreach (int item in array)
                                                          000000e9  mov         eax,dword ptr [ebp-14h] 
                                                          000000ec  mov         edx,dword ptr [ebp-20h] 
                                                          000000ef  cmp         eax,dword ptr [edx+4] 
                                                          000000f2  jl          000000C7 
                                                                      }
                                                          


                                                          Третий цикл:
                                                                      {
                                                                          Console.WriteLine(array[index]);
                                                          000000fc  mov         eax,dword ptr [ebp-10h] 
                                                          000000ff  mov         edx,dword ptr [ebp-1Ch] 
                                                          00000102  cmp         eax,dword ptr [edx+4] 
                                                          00000105  jb          0000010C 
                                                          00000107  call        79033254 
                                                          0000010c  mov         ecx,dword ptr [edx+eax*4+8] 
                                                          00000110  call        048F4D90 
                                                                      for (var index = 0; index < arrayLength; index++)
                                                          00000115  inc         dword ptr [ebp-10h] 
                                                          00000118  mov         eax,dword ptr [ebp-10h] 
                                                          0000011b  cmp         eax,dword ptr [ebp-4] 
                                                          0000011e  jl          000000FC 
                                                          
                                                          0
                                                          Подозреваю что там всё хитро. Не влезая в IL провел несколько тестов производительности For и Foreach.
                                                          Оказалось следующее: если скомпилировать в Release следующий код:
                                                          int s = 0;
                                                          for (int z1 = 0; z1 < array.Lenght; z1++)
                                                          {
                                                          	s += array[z1];
                                                          }
                                                          
                                                          и
                                                          foreach (var i in array)
                                                          {
                                                          	s += i;
                                                          }
                                                          

                                                          то время выполнения идентично (гуляет на 0.3%).

                                                          если же написать так:

                                                          int c = array.Length;
                                                          for (int z1 = 0; z1 < c; z1++)
                                                          {
                                                          	s += array[z1];
                                                          }
                                                          

                                                          то код стабильно выполняется быстрее, чем foreach, где-то на 3%.
                                                            0
                                                            Провел небольшое исследование…
                                                            Код до компиляции:
                                                                        foreach (var y in x)
                                                                            Console.WriteLine(y);
                                                            


                                                            Код после декомпиляции:
                                                            	int[] array = x;
                                                            	for (int i = 0; i < array.Length; i++)
                                                            	{
                                                            		int y = array[i];
                                                            		Console.WriteLine(y);
                                                            	}
                                                            


                                                            Действительно, компилятор C# считает длину массива переменной…
                                                              +2
                                                              Он так не считает, просто его не правильно понимаете, он не будет каждый раз дёргать метод array.get_Length
                                                                0
                                                                Тесты, приведенные выше, показали — если сохранить длину массива в переменную и использовать в цикле for значение переменной, а не array.Lenght, то такой код стабильно на 3% быстрее. Мелочь, но всё же…
                                                                Да и как это, спрашивается, for не будет каждый раз дергать — попробуйте в цикле поменять значение второй переменной из цикла for — на каждом цикле значение этой переменной вычисляется заново.
                                                                  0
                                                                  Я так думаю, что если у вас в цикле что-нибудь более-менее комплексное, то разница будет неощутима. Если же цикл легкий и короткий, то в некоторых случаях его можно переписать на LINQ для читабельности и меньшей вероятности ошибиться в индексах. В большинстве случаев от C# не требуется сверхвысокой производительности и такие вещи как вынос Length в переменную могут только затруднить чтение кода.
                                                                    0
                                                                    Так ведь вопрос не в этом. Дело в том, что если уж компилятор автоматически преобразует foreach в for, то надо было бы делать это оптимально — ведь именно оптимальности ждут от оптимизаций.
                                                                      0
                                                                      Это с одной стороны. А с другой будет увеличение времени компиляции и размера сборки. Как мне кажется, все сделано правильно. Хочешь мегапроизводительность в мелочах — пиши в стиле C. А если нет — используй foreach. Зачем все усложнять?
                                                                        0
                                                                        А с другой будет увеличение времени компиляции и размера сборки.

                                                                        Где увеличение? Как?!

                                                                        Увеличение на 10 байт и 100 наносекунд (беру с сильным запасом) — это так много?
                                                                          –1
                                                                          Где-то столько же вы и выиграете в ран-тайме. А на генерацию локальной переменной по идее все-таки время нужно, нужно же обеспечить уникальность имени — значит, нужно проверять область видимости. Если использовать недопустимые для языка символы в идентификаторе (но допустимые для IL) — тогда нужно проверять и уникальность на случай если несколько циклов в одной области видимости. И даже если их генерировать с циферкой на конце без проверок — на генерацию потребуется время как ни крути и, я так подозреваю, больше, чем 100 наносекунд. Плюс этот код вносит сложность в дальнейшую разработку компилятора. На написание, отладку и тестирование нужно время. Не факт, что руководящие одобрили бы затрату ресурсов на такие мелочи.

                                                                          Вы не думайте, что я не согласен с тем, что можно было бы сделать как вы предлагаете, я лишь говорю что нет ничего страшного в том, что так сделано не было. Я считаю, они знали о такой возможности, но по своим причинам пошли по пути наименьшего сопротивления.
                                                              +1
                                                              Вероятно вы запускали в дебаге или он был подключен? Что получается из трех ваших циклов можете посмотреть выше
                                                                0
                                                                1. Цитата из моего комментария «если скомпилировать в Release следующий код». Само собой, что в DEBUG и под отладкой цифры совсем другие.
                                                                2. Внимательнее посмотрите код циклов — они все 3 разные.
                                                                2.1 Цикл for от 0 до array.Lenght
                                                                2.2 Цикл foreach по элементам array
                                                                2.3 Цикл for от 0 до значения локальной переменной, в которую предварительно записали значение array.Lenght

                                                                а Выше показан результат декомпиляции 1 foreach и двух идентичных циклов for
                                                                  0
                                                                  Если внимательно посмотреть третий цикл, то видно, что for (var index = 0; index < arrayLength; index++), в переменной arrayLength нет точки. Переменная создается до цикла.

                                                                  Во вторых там я хотел показать именно в том примере, что циклы используют индекс, а не через GetEnumerator, поэтому и запостил там, а не тут. Ну и разницы между двумя for вообще никакой нет. Да, foreach немного по другому скомпилировалось на 6 байт больше в том случае, но это ни разу не показатель в таких синтетических тестах.
                                                            0
                                                            Единственным способом выявления таких реализаций является использование рефлексии, например при помощи Object Browser в Visual Studio.

                                                            По-моему проще посмотреть в MSDN )
                                                              0
                                                              Ну это сработает только для стандартных классов .Net.
                                                              А для решения этой проблемы «в общем» приходится использовать какой-то тул с поддержкой рефлексии, самый простой из них — Object Browser.
                                                                0
                                                                Самый простой — посмотреть исходный код. Если его нет — студия восстановит что сможет из метаданных. Получается гораздо нагляднее, чем Object Browser. По крайней мере, для меня.
                                                                  0
                                                                  Понятно, что исходный код — самое лучшее из решений. Но для обзора API все-таки лучше подходят какие-то инструменты вроде Reflector.Net.

                                                                  А по поводу восстановление класса из метаданных — я не совсем уверен, что это работает во всех версиях…
                                                                    0
                                                                    Класс из метаданных не восстановится никак.

                                                                    Но у .NET открыты исходники. И студия умеет их выкачивать и показывать.

                                                                    А «инструменты вроде Reflector.Net.» — это декомпиляторы.
                                                                      0
                                                                      А мне не нужно восстанавливать весь класс — мне достаточно восстановить сигнатуры классов и методов, а для этого метаинформации более чем достаточно.

                                                                      Я знаком с возможностями VS по подгрузке исходных кодов .Net, но это, как я отметил выше, не универсальное решение.
                                                              +1
                                                              Всегда удивляло, зачем работодатели требуют знания таких вещей наизусть когда есть документация. Разве что на случай если Интернет отключат… Можно, конечно, использовать как косвенный индикатор опыта, но по-моему единственная проверка, что имеющая смысл при найме программиста — пара практических тестовых заданий (например на реализацию и на дебаг) с последующим анализом качества кода, при этом как и в реальных условиях у человека в «дано» должна быть полноценная IDE (в случае C# — актуальная VisualStudio) и свободный доступ в Интернет. Если только это не набор программистов в экипаж, отправляющийся на Марс…
                                                                +1
                                                                Массивы и цикл foreach — это базовые части языка, их нужно знать без документации.
                                                                  0
                                                                  Ну это было второе (из трех) собеседований, причем оно состояло из трех частей:
                                                                  1. Письменного теста (вот оттуда этот пример).
                                                                  2. Теста на компьютере (написать в Visual Studio сначала набор unit tests для метода, рассчитывающего медиану, а потом и запрограммировать этот метод).
                                                                  3. Устное собеседование с двумя Senior Developers.

                                                                  Каждая часть длилась по часу. По мне — так вполне адекватно для первой части: проверки на «вшивость», знаешь ли ты вообще, что такое цикл и .Net.
                                                                    0
                                                                    Т.е. если неправильно ответить на этот вопрос, то ты не знаешь, что такое цикл? :)
                                                                    Я точно знаю и использовал foreach для int[], но не задумывался, реализует ли он IList.
                                                                      0
                                                                      Ну, там было в тесте три разных раздела (базовые вещи, .Net и базы данных), в каждом по 30-35 вопросов и все на один час. Я не думаю, что неправильный ответ тут сразу бы привел к дисквалификации, скорее вызвал бы удивление, если все остальное было бы неправильно.

                                                                      В общем и целом я думаю, учитывается совокупность факторов, и тот факт, что все это делается в стрессовых условиях.
                                                                  +1
                                                                  Покажусь снобом, или ещё кем-нибудь плохим, но не зная про требование foreach к классу, про массив и IList, про то где находится Count, вам не стоило упоминать про Senior .Net Developer. Это просто смешно. Такие вещи знают обычно Junior'ы с годом опыта которые прочли пару книжек по C#/.NET

                                                                    0
                                                                    О, как я ждал этого комментария! :)

                                                                    Действительно, Вы покажетесь снобом или «кем-нибудь плохим», особенно учитывая тот факт, что на работу меня взяли (да, именно Senior Developer).

                                                                    Вы, конечно, это тоже все знали после года работы Junior и прочтения «пары книжек» по .Net, не так ли?

                                                                    «Это просто смешно».
                                                                      0
                                                                      Я, к примеру, знал, еще не имея года коммерческой разрабоки на .NET, а только just4fun, в рамках курсовых и лабораторных. Да и джуниором я никогда не был в общепризнаном понимании. Книжек я не читал. Да и формальное название роли никакого отношения к реальности не имеет.

                                                                      PS: сколько Senior .NET Developer зарабатывает в Германии?
                                                                        0
                                                                        И что же Вы знали?

                                                                        .NET Senior Developer зарабатывает столько, на сколько он сможет договориться :)
                                                                          0
                                                                          Знал то, про что выше написал GrigoryPerepechko.

                                                                          .NET Senior Developer зарабатывает столько, на сколько он сможет договориться :)
                                                                          Извините, но это чушь.
                                                                            0
                                                                            > Знал то, про что выше написал GrigoryPerepechko.

                                                                            Ну, то что он выше написал я тоже, честно говоря, не понял — что конкретно он имеет в виду. Поэтому мне было интересно, что конкретно Вы знали, разрабатывая just4fun.

                                                                            Я спрашиваю потому, что я собственно не только работаю много лет как разработчик, я и преподаю .Net как MCT и те же Collections входят в базовый курс .Net (раньше экзамен 70-536), который я преподавал несколько раз как тренер. Но утверждать, что это всё знают junior и в рамках собеседования легко ответят не все это, особенно в той патетичной манере, как это изложено — честно скажу, не верю. Как говорится «на миру и смерть красна» — легко заявлять о чем-то, когда ты сидишь в браузере и печатаешь комментарий…

                                                                            По поводу зарплат — что Вас не устраивает? В Германии нет тарифной сетки для работающих в частных структурах, поэтому зарплата Senior Developer полностью зависит от того, насколько он договорится с работодателем — и эта зарплата может колебаться в 2-3 раза в зависимости от фирмы, земли и места работы…
                                                                              0
                                                                              По поводу зарплат — что Вас не устраивает? В Германии нет тарифной сетки для работающих в частных структурах, поэтому зарплата Senior Developer полностью зависит от того, насколько он договорится с работодателем — и эта зарплата может колебаться в 2-3 раза в зависимости от фирмы, земли и места работы…
                                                                              От земли — может быть. Остальное ерунда, т.к. зарплата полностью подчиняется закону рынка. И, таким образом, при условии, что «Senior Developer» это некий стандарт, то все такие программисты со временем будут работать в одном месте — там где зарплата в 3 раза выше. Либо везде будет примерно одинаковая зарплата.

                                                                              я и преподаю .Net как MCT
                                                                              Тогда я не понимаю, как вы могли не знать, что Array реализует IList`1, что Count это свойство ICollection`1 и ICollection, и про explicit interface implementation? — Про это и пишет GrigoryPerepechko
                                                                                0
                                                                                > Остальное ерунда, т.к. зарплата полностью подчиняется закону рынка. И, таким образом, при условии, что «Senior Developer» это некий стандарт, то все такие программисты со временем будут работать в одном месте — там где зарплата в 3 раза выше.

                                                                                А уже несколько раз пытаюсь Вам сказать, что это — не «некий стандарт», это скорее описание позиции, где (практически) с первого дня ожидается работа над продуктивным кодом и где никто не настроен сначала тебя обучать тем технологиям, с которыми нужно работать. Поэтому Senior Developer получает столько, насколько он договорился в интервью. Неужели Вы никогда не видели вакансии «зарплата по результатам собеседования»?

                                                                                >Тогда я не понимаю, как вы могли не знать, что Array реализует IList`1, что Count это свойство ICollection`1 и ICollection, и про explicit interface implementation?
                                                                                Наверное если бы меня целенаправленно спросили об этом, я бы подумав восстановил всю структуру наследования коллекций и их интерфейсов в голове — но вопрос был поставлен по-другому и это был один из 90 (!) вопросов на которые я должен был ответить в течение часа.
                                                                                Тем не менее, даже формально когда-то заучив это для сертификаций и преподавания я, честно признаюсь, не понимал что речь в данном случае идет о explicit implementation, и именно осознание этого факта показалось мне достаточно интересным для статьи.
                                                                                Вы же утверждаете, что для Вас это все было обыденным знанием и все наследование Вы всегда можете его воспроизвести «от зубов»?
                                                                                  0
                                                                                  Вы же утверждаете, что для Вас это все было обыденным знанием и все наследование Вы всегда можете его воспроизвести «от зубов»?
                                                                                  Я этого нигде не утверждал.
                                                                                  Тем не менее, даже формально когда-то заучив это для сертификаций и преподавания я, честно признаюсь, не понимал что речь в данном случае идет о explicit implementation, и именно осознание этого факта показалось мне достаточно интересным для статьи.
                                                                                  Это элементарно, Ватсон.

                                                                                  1. То что Array реализует IList`1 _должно_ быть известно всем, т.к. с массивами и всякими списками все работают постоянно.
                                                                                  2. У IList`1 где-то в иерархии есть поле Count.
                                                                                  3. У Array этого поля нет.
                                                                                  4. Следовательно: оно было реализовано через explicit implementation, т.к. нельзя оставлять члены интерфейса без реализации.

                                                                                  Тем не менее, даже формально когда-то заучив это для сертификаций и преподавания я, честно признаюсь, не понимал что речь в данном случае идет о explicit implementation, и именно осознание этого факта показалось мне достаточно интересным для статьи.
                                                                                  В этом и проблема: вы их заучили, а я это знаю, потому что с этим работаю.

                                                                                  Неужели Вы никогда не видели вакансии «зарплата по результатам собеседования»?
                                                                                  Не знаю как у вас в Германии, а в России за этой фразой обычно прячут неприлично низкую зарплату. У кого высокие зарплаты — не скрывают суммы.

                                                                                  А уже несколько раз пытаюсь Вам сказать, что это — не «некий стандарт», это скорее описание позиции, где (практически) с первого дня ожидается работа над продуктивным кодом и где никто не настроен сначала тебя обучать тем технологиям, с которыми нужно работать.
                                                                                  Поэтому Senior Developer получает столько, насколько он договорился в интервью.
                                                                                  1. Я прекарсно осознаю что значит Senior .NET Developer. 2. Между первой и второй фразой нет никакой связи. 3. Работодатель заранее знает, сколько он будет платить кандидату — так называемая «вилка».
                                                                      0
                                                                      Просвятите, как это сакральное знание помогает сеньору в работе?
                                                                        0
                                                                        Изящный код и красивые архитектуры часто являются результатом применения вот таких вот сакральных знаний. Умея, к примеру, Java на уровне «senior» вы можете писать хороший код на C#, но это будет C# код Java программиста. Мелочи позволяют использовать язык по максимуму.
                                                                      +2
                                                                      >Чтобы получить доступ к имплементации необходимо кастовать инстанцию класса к интерфейсу
                                                                      доставило.
                                                                        0
                                                                        Я рад, что смог доставить Вам что-то… надеюсь удовольствие… :)
                                                                          0
                                                                          Должны были бы знать по фильму, что заклинания из чёрной книги читать нельзя… ;-)

                                                                        Only users with full accounts can post comments. Log in, please.