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

в 4:16, , рубрики: .net, implicit, interface, интерфейсы, ооп, метки: , ,

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

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

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

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

Класс 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.

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

Мне сразу бросилась в глаза моя ошибка: Count было свойством не IList, а ICollection, еще предыдущего интерфейса в цепочке наследования. Тем не менее, сам массив уже не имел такого свойства, хотя другие свойства этого интерфейса, 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

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

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

Явная имплементация интерфейса, согласно 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 (вероятно это является попыткой поддержания совместимости с С++ и Java). Таким образом, мы можем скрывать отдельные методы имплементированного интерфейса и не скрывать (=делать публичными) другие.

Здесь, правда, возникает такая проблема, что во многих случаях совершенно невозможно догадаться о том, какие интерфейсы имплементированы классом «неявно», поскольку ни методы, ни свойства этих интерфейсов не видны в IntelliSense. Единственным способом выявления таких имплементаций является использование рефлексии, например при помощи 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» в первом вопросе?

Автор: alaudo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js