Skip to content

Instantly share code, notes, and snippets.

@vertigra
Last active February 24, 2017 13:05
Show Gist options
  • Select an option

  • Save vertigra/14ed3b09979b357341ab83deffc07201 to your computer and use it in GitHub Desktop.

Select an option

Save vertigra/14ed3b09979b357341ab83deffc07201 to your computer and use it in GitHub Desktop.
Интерфейсы (С#)

Интерфесы (С#)

Конспект статьи

Интерфейс (interface) представляет собой не более чем просто именованный набор абстрактных членов. Абстрактные методы являются чистым протоколом, поскольку не имеют никакой стандартной реализации. Конкретные члены, определяемые интерфейсом, зависят от того, какое поведение моделируется с его помощью. каждый класс (или структура) может поддерживать столько интерфейсов, сколько необходимо, и, следовательно, тем самым поддерживать множество поведений.

В интерфейсе ни у одного из методов не должно быть тела. Это означает, что в интерфейсе вообще не предоставляется никакой реализации. В нем указывается только, что именно следует делать, но не как это делать. Как только интерфейс будет определен, он может быть реализован в любом количестве классов. Кроме того, в одном классе может быть реализовано любое количество интерфейсов.

Для реализации интерфейса в классе должны быть предоставлены тела (т.е. конкретные реализации) методов, описанных в этом интерфейсе. Каждому классу предоставляется полная свобода для определения деталей своей собственной реализации интерфейса. Следовательно, один и тот же интерфейс может быть реализован в двух классах по-разному. Тем не менее в каждом из них должен поддерживаться один и тот же набор методов данного интерфейса. А в том коде, где известен такой интерфейс, могут использоваться объекты любого из этих двух классов, поскольку интерфейс для всех этих объектов остается одинаковым. Благодаря поддержке интерфейсов в C# может быть в полной мере реализован главный принцип полиморфизма: один интерфейс — множество методов.

Интерфейсы объявляются с помощью ключевого слова interface. Например:

interface имя
{
    возвращаемый_тип имя_метода (список_параметоров);
    ....
}

В объявлении методов интерфейса используются только их возвращаемый_тип и сигнатура. Они, по существу, являются абстрактными методами. Как пояснялось выше, в интерфейсе не может быть никакой реализации. Поэтому все методы интерфейса должны быть реализованы в каждом классе, включающем в себя этот интерфейс. В самом же интерфейсе методы неявно считаются открытыми, поэтому доступ к ним не нужно указывать явно.

Помимо методов, в интерфейсах можно также указывать свойства, индексаторы и события. Интерфейсы не могут содержать члены данных. В них нельзя также определить конструкторы, деструкторы или операторные методы. Кроме того, ни один из членов интерфейса не может быть объявлен как static.

Как только интерфейс будет определен, он может быть реализован в одном или нескольких классах. Для реализации интерфейса достаточно указать его имя после имени класса, аналогично базовому классу. Например:

class имя_класса : имя_интерфейса 
{
    // тело класса
}

Если интерфейс реализуется в классе, то это должно быть сделано полностью. Реализовать интерфейс выборочно и только по частям нельзя.

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

Методы, реализующие интерфейс, должны быть объявлены как public. Дело в том, что в самом интерфейсе эти методы неявно подразумеваются как открытые, поэтому их реализация также должна быть открытой. Кроме того, возвращаемый тип и сигнатура реализуемого метода должны точно соответствовать возвращаемому типу и сигнатуре, указанным в определении интерфейса.

Например:

Класс и его интерфейс:

public class TelegramCredentials
{  
    public string token { get; set; }
    public string chatId { get; set;  }
}

public interface ITelegramCredentials
{
    TelegramCredentials GetTelegramCredentials();
}

Реализация в тесте:

[TestFixture]
internal class TestTelegramCredentials : ITelegramCredentials
{
    private const string mTestToken = "527923125:AARlRhNIVpcjb8EUbvMwqgKyxPv0Z6FWtXC";
    private const string mTestChatId = "-467814538";

    [Test]
    public void TestSetGetTokenChatId()
    {
        TelegramCredentials telegramCredentials = GetTelegramCredentials();

        Assert.AreEqual(telegramCredentials.token, mTestToken);
        Assert.AreEqual(telegramCredentials.chatId, mTestChatId);
    }

    public TelegramCredentials GetTelegramCredentials()
    {
        return new TelegramCredentials { chatId = mTestChatId, token = mTestToken };
    }
}

Метод GetTelegramCredentials() может реализован абсолютно любым способом, главное что бы он возвращал TelegramCredentials.

Например:

public TelegramCredentials GetTelegramCredentials()
{
    return new TelegramCredentials { chatId = "-467814538", token = "527923125:AARlRhNIVpcjb8EUbvMwqgKyxPv0Z6FWtXC" };
}

Интерфейсные ссылки

в C# допускается объявлять переменные ссылочного интерфейсного типа, т.е. переменные ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейс. При вызове метода для объекта посредством интерфейсной ссылки выполняется его вариант, реализованный в классе данного объекта. Этот процесс аналогичен применению ссылки на базовый класс для доступа к объекту производного класса.

Переменной ссылки на интерфейс доступны только методы, объявленные в ее интерфейсе. Поэтому интерфейсную ссылку нельзя использовать для доступа к любым другим переменным и методам, которые не поддерживаются объектом класса, реализующего данный интерфейс.

Например класс работающий с отправкой сообщений в Telegram:

public class TelegramClientUtils : ITelegramClientsUtil
{
    private readonly TelegramBotClient mClient;
    private readonly int mChatId;

    public TelegramClientUtils(TelegramCredentials telegramCredentials)
    {
        mClient = new TelegramBotClient()
        {
            Token = telegramCredentials.token
        };

        mChatId = int.Parse(telegramCredentials.chatId);
    }

    public bool SendMessage(string message)
    {
        try
        {
            return mClient.SendMessage(mChatId, message).Ok;
        }
        catch (Exception)
        {
            return false;
        }
    }
    
    public string GetUserName()
    {
        return mClient.GetMe().UserName;
    }
}

public interface ITelegramClientsUtil
{
    bool SendMessage(string message);
    string GetUserName();
}

Теперь доступ к метода SendMessage и GetUserName() можно получить так.

[TestFixture]
internal class TestTelegramClientUtils : ITelegramCredentials
{
    private TelegramBotClient mClient;

    [SetUp]
    public void OnTestStart()
    {
        mClient = new TelegramBotClient()
        {
        Token = GetTelegramCredentials().token
        };
    }

    [Test]
    public void TestSendMessage()
    {
        ITelegramClientsUtil iTelegramClientsUtil = new TelegramClientUtils(GetTelegramCredentials());

        Assert.True(iTelegramClientsUtil.SendMessage("Test Mesage"));
    }

    [Test]
    public void TestGetUserName()
    {
        ITelegramClientsUtil iTelegramClientsUtil = new TelegramClientUtils(GetTelegramCredentials());

        string userName = iTelegramClientsUtil.GetUserName();

        Assert.AreEqual(userName, mClient.GetMe().UserName);
    }

    public TelegramCredentials GetTelegramCredentials()
    {
        return WindowsCredentialUtils.GetCredential("TestTelegramBotName");
    }
}

Ключевое слово as

Определить, поддерживает ли данный тип тот или иной интерфейс, можно с использованием ключевого слова as. Если объект удается интерпретировать как указанный интерфейс, то возвращается ссылка на интересующий интерфейс, а если нет, то ссылка null. Следовательно, перед продолжением в коде необходимо предусмотреть проверку на null.

IInfo obj = ui1 as IInfo;
if (obj != null)
   Console.WriteLine("Тип UI поддерживает интерфейс IInfo");
else
   Console.WriteLine(":(");

Ключевое слово is

Проверить, был ли реализован нужный интерфейс, можно также с помощью ключевого слова is. Если запрашиваемый объект не совместим с указанным интерфейсом, возвращается значение false, а если совместим, то можно спокойно вызывать члены этого интерфейса без применения логики try/catch.

if (ui1 is IInfo)
  Console.WriteLine("Тип UI поддерживает интерфейс IInfo");
else
  Console.WriteLine(":(");

Интерфейсные свойства и индексаторы

Интерфейсные свойства

Аналогично методам, свойства указываются в интерфейсе вообще без тела. Ниже приведена общая форма объявления интерфейсного свойства:

тип имя
{
    get;
    set;
}

В определении интерфейсных свойств, доступных только для чтения или только для записи, должен присутствовать единственный аксессор: get или set соответственно.

Несмотря на то что объявление свойства в интерфейсе очень похоже на объявление автоматически реализуемого свойства в классе, между ними все же имеется отличие. При объявлении в интерфейсе свойство не становится автоматически реализуемым. В этом случае указывается только имя и тип свойства, а его реализация предоставляется каждому реализующему классу. Кроме того, при объявлении свойства в интерфейсе не разрешается указывать модификаторы доступа для аксессоров. Например, аксессор set не может быть указан в интерфейсе как private.

using System;

namespace ConsoleApplication1
{
    interface IUserInfo
    {
        string Name
        {
            get;
            set;
        }
    }

    class UI : IUserInfo
    {
        string myName;

        public string Name
        {
            set
            {
                myName = value;
            }

            get
            {
                return myName;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            UI user1 = new UI();
            user1.Name = "Alexandr";
            
            Console.ReadLine();
        }
    }
}

Интерфейсные индексаторы

В интерфейсе можно также указывать индексаторы. Ниже приведена общая форма объявления интерфейсного индексатора:

тип_элемента this[int индекс]
{
    get;
    set;
}

Например:

interface IUserInfo
{
    string Name
    {
        get;
        set;
    }

    string this[int index]
    {
        get;
        set;
    }        
}

class UI : IUserInfo
{
    string myName;

    public string Name
    {
        set
        {
            myName = value;
        }

        get
        {
            return myName;
        }
    }

    public string this[int index]
    {
        set { myName = value; }
        get { return myName; }
    }
}

class Program
{
    static void Main()
    {
        UI user1 = new UI();
        user1.Name = "Alexandr";
        user1[5] = "Dmitryi";
        user1[10] = "Alexey";
            
        Console.ReadLine();
    }
}

Наследование интерфейсов

Один интерфейс может наследовать другой. Синтаксис наследования интерфейсов такой же, как и у классов. Когда в классе реализуется один интерфейс, наследующий другой, в нем должны быть реализованы все члены, определенные в цепочке наследования интерфейсов.

Таким образом, интерфейсы могут быть организованы в иерархии. Как и в иерархии классов, в иерархии интерфейсов, когда какой-то интерфейс расширяет существующий, он наследует все абстрактные члены своего родителя (или родителей). Конечно, в отличие от классов, производные интерфейсы никогда не наследуют саму реализацию. Вместо этого они просто расширяют собственное определение за счет добавления дополнительных абстрактных членов.

Использовать иерархию интерфейсов может быть удобно, когда нужно расширить функциональность определенного интерфейса без нарушения уже существующих кодовых баз.

public interface A
{
    int Sum();
}

// Унаследованный интерфейс
public interface B : A
{
    int Del();
}

class MyOperation : B
{
    int x = 10, y = 5;

    public int Sum()
    {
        return x + y;
    }

    public int Del()
    {
        return x / y;
    }
}

В отличие от классов, один интерфейс может расширять сразу несколько базовых интерфейсов.

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

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

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

Для явной реализации интерфейсного метода могут быть две причины. Во-первых, когда интерфейсный метод реализуется с указанием его полного имени, то такой метод оказывается доступным не посредством объектов класса, реализующего данный интерфейс, а по интерфейсной ссылке. Следовательно, явная реализация позволяет реализовать интерфейсный метод таким образом, чтобы он не стал открытым членом класса, предоставляющего его реализацию. И во-вторых, в одном классе могут быть реализованы два интерфейса с методами, объявленными с одинаковыми именами и сигнатурами. Но неоднозначность в данном случае устраняется благодаря указанию в именах этих методов их соответствующих интерфейсов (еще про явную реализацию у Шилдта)

public interface IName
{
    void WriteName();
}

public interface INameFamily
{
    // Объявляем в данном интерфейсе такой же метод
    void WriteName();
    void WriteFamily();
}

public interface IUserInfo : INameFamily
{
    // Обязательно нужно указать ключевое слово new
    // чтобы не скрывались методы базового интерфейса
    new void WriteName();
    void WriteUserInfo();
}

// Класс, реализующий два интерфейса
class UserInfo : IUserInfo,IName
{
    string ShortName, Family, Name;

    public UserInfo(string Name, string Family, string ShortName)
    {
        this.Name = Name;
        this.Family = Family;
        this.ShortName = ShortName;
    }

    // Используем явную реализацию интерфейсов
    // для исключения неоднозначности
    void IName.WriteName()
    {
        Console.WriteLine("Короткое имя: " + ShortName);
    }

    void INameFamily.WriteFamily()
    {
        Console.WriteLine("Фамилия: " + Family);
    }

    void INameFamily.WriteName()
    {
        Console.WriteLine("Полное имя: " + Name);
    }

    void IUserInfo.WriteName() { }

    public void WriteUserInfo()
    {
        UserInfo obj = new UserInfo(Name, Family, ShortName);
        // Для использования закрытых методов необходимо
        // создать интерфейсную ссылку
        IName link1 = (IName)obj;
        link1.WriteName();
        INameFamily link2 = (INameFamily)obj;
        link2.WriteName();
        link2.WriteFamily();
        IUserInfo link3 = (IUserInfo)obj;
        link3.WriteName();
    }
}

class Program
{
    static void Main()
    {
        UserInfo obj = new UserInfo(Name: "Alexandr", ShortName: "Alex", Family: "Erohin");
        obj.WriteUserInfo();
        Console.ReadLine();
    }
}

Из Шилдта про явную реализацию

Необходимость явной реализации для члена интерфейса может обосновываться двумя причинами. Во-первых, класс может реализовывать два интерфейса, причем в обоих объявляются методы с помощью одного и того же имени и типа сигнатуры. Благодаря полному определению имен устраняется двухсмысленность подобной ситуации. Во-вторых, при реализации метода посредством его полностью определенного имени определяется объем частной реализации, который не доступен для кода, не относящийся к классу.

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

Пример отсюда с интеграционным тестом (NSubtitute)

Интерфейс:

public interface IFileSystemHelper
{
    List<GetListFromFile>(string path, Encoding enc);
}

Реализация интерфейса:

public class FileSystemHelper : IFileSystemHelper
{
    public List<string> GetListFromFile(string path, Encoding enc)
    {
        try
        {
            List<string> list = new List<string>();
            StreamReader reader = new StreamReader(path, enc);
 
            using (reader)
            {
                while (!reader.EndOfStream)
                {
                    string word = reader.ReadLine().Trim();
                    if (word != "")
                        list.Add(word);
                }
            }
            return list;
        }
        catch (Exception)
        {
            return null;
        }
    }
}

Пример использования:

ublic class FileUtils
{
    private static IFileSystemHelper fileSystemHelper;
    public static IFileSystemHelper FileSystemHelper
    {
        get
        {
            if (fileSystemHelper == null)
            {
                fileSystemHelper = new FileSystemHelper();
            }
            return fileSystemHelper;
        }
        set { fileSystemHelper = value; }
    }
 
    public static List<string> GetListFromFile(string path, Encoding enc)
    {
        return FileSystemHelper.GetListFromFile(path, enc);
    }
}

Пример теста:

private static void PrepareFakeGetListFromFileWithContent(string content)
{
    //создаем фейк на основе интерфейса IFileSystemHelper
    var fileSystemHelper = Substitute.For<IFileSystemHelper>(); 
    // выставляем его классу FileUtils, теперь при обращении к любому методу класса будет использоваться наш фейк а не реальный класс FileSystemHelper
    FileUtils.FileSystemHelper = fileSystemHelper;
 
    //а здесь мы указываем, что при вызове у нашего фейка метода GetListFromFile с любым путем (Arg.Any<string>()) и любой кодировкой (Arg.Any<Encoding>()) будет возвращен список сформированный из переданного параметра content, если он не был равен null, иначе Null и возвращаем - эмулирую отсутствие файла
    List<string> returnThis = null;
    if(content != null)
    {
        returnThis = new List<string>(content.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries));
    }
    fileSystemHelper.GetListFromFile(Arg.Any<string>(), Arg.Any<Encoding>()).Returns(returnThis);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment