Pull to refresh

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

Reading time 8 min
Views 49K
В прошедний понедельник мне посчастливилось попасть на собеседование на 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» в первом вопросе?
Tags:
Hubs:
+46
Comments 91
Comments Comments 91

Articles