Какова цель сокрытия (с использованием модификатора new) объявления метода интерфейса?

можно пометить объявление метода в интерфейсе как «новое», но имеет ли оно какой-либо «технический» смысл или это просто способ явно указать, что объявление не может переопределить предыдущее?

Например :

interface II1
{
    new void F();
}

interface II2 : II1
{
    new void F();
}

действителен (компилятор С# 4.0 не жалуется), но не отличается от:

interface II1
{
    void F();
}

interface II2 : II1
{
    void F();
}

Заранее спасибо за любую информацию.

РЕДАКТИРОВАТЬ: знаете ли вы сценарий, в котором было бы полезно спрятаться в интерфейсе?

EDIT: Согласно этой ссылке: Скрывает ли метод когда-либо хорошая идея (спасибо, Скотт), наиболее распространенным сценарием является эмуляция ковариантного возвращаемого типа.


person Pragmateek    schedule 13.08.2010    source источник


Ответы (7)


Второй пример выдает следующее предупреждение компилятора:

'II2.F()' скрывает унаследованный член 'II1.F()'. Используйте новое ключевое слово, если предполагалось скрытие.

Я бы сказал, что разница использования ключевого слова new именно в этом: демонстрация намерения.

person Fredrik Mörk    schedule 13.08.2010
comment
Спасибо за Ваш ответ. То есть это не повлияет на использование интерфейсов? Я отредактирую исходный пост, чтобы уточнить это. - person Pragmateek; 13.08.2010
comment
@Serious: нет, единственная разница - это предупреждение компилятора. Я бы рекомендовал использовать ключевое слово new для ясности. Ну, нет, на самом деле я бы рекомендовал не скрывать методы вообще, так как я нахожу это запутанным, но если необходимо, используйте ключевое слово new. - person Fredrik Mörk; 13.08.2010
comment
Вы правы, я тоже не вижу ни одного интересного варианта использования, где это должно быть нужно. - person Pragmateek; 13.08.2010
comment
@Серьезно, для этого есть несколько вариантов использования. См. stackoverflow.com/questions/ 2663274/ - person ScottS; 13.08.2010
comment
@ Скотт, большое спасибо за эту ссылку, я соответственно обновил начальный пост. - person Pragmateek; 16.08.2010

Они очень разные. Используя «новый», вы создаете новую цепочку наследования. Это означает, что любые реализации II2 должны будут реализовывать обе версии F(), и фактическая версия, которую вы в конечном итоге вызовете, будет зависеть от типа ссылки.

Рассмотрим следующие три реализации:

    class A1 : II1
    {
        public void F()
        {
            // realizes II1.F()
        }
    }

    class A2 : II2
    {
        void II1.F()
        {
            // realizes II1.F()
        }

        void II2.F()
        {
            // realizes II2.F()
        }
    }

    class A3 : II2
    {
        public void F()
        {
            // realizes II1.F()
        }

        void II2.F()
        {
            // realizes II2.F()
        }
    }

Если у вас есть ссылка на A2, вы не сможете вызвать ни одну из версий F() без предварительного приведения к II1 или II2.

A2 a2 = new A2();
a2.F(); // invalid as both are explicitly implemented
((II1) a2).F(); // calls the II1 implementation
((II2) a2).F(); // calls the II2 implementation

Если у вас есть ссылка на A3, вы сможете напрямую вызвать версию II1, так как это неявное внедрение:

A3 a3 = new A3();
a3.F(); // calls the II1 implementation
((II2) a3).F(); // calls the II2 implementation
person Paul Ruane    schedule 13.08.2010
comment
обратите внимание, что нет никакой разницы в скомпилированном коде между двумя примерами, приведенными в вопросе. Разница, которую вы объясняете, заключается в эффекте сокрытия метода, но это происходит независимо от того, используется ли ключевое слово new или нет. - person Fredrik Mörk; 13.08.2010
comment
Спасибо за эти прекрасные примеры. - person Pragmateek; 13.08.2010
comment
@Fredrik: ой, спасибо. Я почему-то совершенно проглядел это. Вы действительно правы, и предупреждение - единственная разница. - person Paul Ruane; 16.08.2010
comment
@PaulRuane Не могли бы вы обновить ответ, чтобы включить понимание Фредрика? - person Colm Bhandal; 28.05.2020

Я знаю одно хорошее применение для этого: вы должны сделать это, чтобы объявить COM-интерфейс, производный от другого COM-интерфейса. Об этом говорится в этой статье журнала.

Между прочим, автор совершенно неверно определяет источник проблемы, он не имеет ничего общего с «налогом на наследство». COM просто использует способ, которым типичный компилятор C++ реализует множественное наследование. Для каждого базового класса существует одна v-таблица, как раз то, что нужно COM. CLR этого не делает, она не поддерживает MI и существует только одна v-таблица. Методы интерфейса объединены в v-таблицу базового класса.

person Hans Passant    schedule 13.08.2010

Пример после ответа Фредрика. Я хочу иметь интерфейс, представляющий метод для получения объекта по идентификатору. Затем я хочу, чтобы в качестве этого интерфейса можно было использовать как службу WCF, так и какой-либо другой стандартный репозиторий. Чтобы поиграть с WCF, мне нужно украсить свой интерфейс атрибутами. С точки зрения намерения, я не хочу украшать свой интерфейс материалами WCF, если он не будет использоваться только через WCF, поэтому мне нужно использовать новый. Вот код:

public class Dog
{
    public int Id { get; set; }
}

public interface IGetById<TEntity>
{
    TEntity GetById(int id);
}

public interface IDogRepository : IGetById<Dog> { }

public class DogRepository : IDogRepository
{
    public Dog GetById(int id)
    {
        throw new NotImplementedException();
    }
}

[ServiceContract]
public interface IWcfService : IGetById<Dog>
{
    [OperationContract(Name="GetDogById")]
    new Dog GetById(int id);
}
person greyalien007    schedule 01.12.2013
comment
Интересно, это из реального варианта использования? - person Pragmateek; 02.12.2013
comment
Да, все мои методы извлечения из репозитория абстрагированы за интерфейсом (по одному на метод). Тогда любой объект, который должен поддерживать какой-либо поиск, может реализовать интерфейс. Некоторые из них являются службами WCF, что приводит к приведенному выше коду. - person greyalien007; 02.12.2013

Новый модификатор очень прост, он просто подавляет предупреждение компилятора, которое создается, когда метод скрыт. При использовании в методе, который не скрывает другой метод, генерируется предупреждение.

Из Спецификации языка C# 3.0

10.3.4 Новый модификатор Объявление-члена-класса позволяет объявлять член с тем же именем или сигнатурой, что и унаследованный член. Когда это происходит, считается, что член производного класса скрывает член базового класса. Скрытие унаследованного члена не считается ошибкой, но вызывает предупреждение компилятора. Чтобы подавить предупреждение, объявление члена производного класса может включать новый модификатор, указывающий, что производный член предназначен для скрытия базового члена. Эта тема обсуждается далее в §3.7.1.2. Если в объявление включен новый модификатор, который не скрывает унаследованный член, выдается соответствующее предупреждение. Это предупреждение подавляется удалением нового модификатора.

person ScottS    schedule 13.08.2010

Прочитав эти ответы, они довольно хорошо объясняют, что делает новый оператор, но я не вижу четкого ответа на эту часть вопроса ОП:

Знаете ли вы сценарий, в котором было бы полезно спрятаться в интерфейсе?

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

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

https://dotnetfiddle.net/kRQpoU

Вот еще раз:

using System;

public class Program
{
    public static void Main()
    {
        //Simple usage
        var simpleCuboid = new MockCuboidSimple();
        var heightUser = new HeightUserEntangled();
        var volumeUser = new VolumeUserEntangled();
        Console.WriteLine("*** Simple Case ***");
        Console.WriteLine(heightUser.PrintHeight(simpleCuboid));
        Console.WriteLine(volumeUser.PrintVolume(simpleCuboid));

        //Smart usage - the same behaviour, but more testable behind the scenes!
        var smartCuboid = new MockCuboidSmart();
        var heightUserSmart = new HeightUserSmart();
        var volumeUserSmart = new VolumeUserSmart();
        Console.WriteLine("*** smart Case ***");
        Console.WriteLine(heightUserSmart.PrintHeight(smartCuboid));
        Console.WriteLine(volumeUserSmart.PrintVolume(smartCuboid));
    }
}

//Disentangled

class VolumeUserSmart
{
    public string PrintVolume(IThingWithVolume volumeThing)
    {
        return string.Format("Object {0} has volume {1}", volumeThing.Name, volumeThing.Volume);
    }       
}

class HeightUserSmart
{
    public string PrintHeight(IThingWithHeight heightThing)
    {
        return string.Format("Object {0} has height {1}", heightThing.Name, heightThing.Height);
    }       
}

class MockCuboidSmart : ICuboidSmart
{
    public string Name => "Mrs. Cuboid";
    public double Height => 3.333;
    public double Width => 31.23432;
    public double Length => 123.12;
    public double Volume => Height * Width * Length;
}

interface ICuboidSmart : IThingWithHeight, IThingWithVolume
{
    //Here's where we use new, to be explicit about our intentions
    new string Name {get;}
    double Width {get;}
    double Length {get;}
    //Potentially more methods here using external types over which we have no control - hard to mock up for testing
}

interface IThingWithHeight
{
    string Name {get;}
    double Height {get;}
}   

interface IThingWithVolume
{
    string Name {get;}
    double Volume {get;}
}

//Entangled

class VolumeUserEntangled
{
    public string PrintVolume(ICuboidSimple cuboid)
    {
        return string.Format("Object {0} has volume {1}", cuboid.Name, cuboid.Volume);
    }       
}

class HeightUserEntangled
{
    public string PrintHeight(ICuboidSimple cuboid)
    {
        return string.Format("Object {0} has height {1}", cuboid.Name, cuboid.Height);
    }       
}

class MockCuboidSimple : ICuboidSimple
{
    public string Name => "Mrs. Cuboid";
    public double Height => 3.333;
    public double Width => 31.23432;
    public double Length => 123.12;
    public double Volume => Height * Width * Length;
}

interface ICuboidSimple
{
    string Name {get;}
    double Height {get;}
    double Width {get;}
    double Length {get;}
    double Volume {get;}
    //Potentially more methods here using external types over which we have no control - hard to mock up for testing
}

Обратите внимание, что VolumeUserSmart и HeightUserSmart зависят только от тех фрагментов интерфейса ICuboidSmart, которые им нужны, а именно IThingWithHeight и IThingWithVolume. Таким образом, они могут быть максимально повторно использованы, т.е. для фигур, отличных от кубоидов, а также их легче проверить. Последний пункт, как я нахожу на практике, имеет решающее значение. Гораздо проще смоделировать интерфейс с меньшим количеством методов, особенно если методы основного типа интерфейса содержат ссылки на типы, которые мы не контролируем. Конечно, всегда можно обойти это с помощью мок-фреймворка, но я предпочитаю, чтобы код оставался чистым в своей основе.

Итак, при чем здесь ключевое слово new? Что ж, поскольку VolumeUserSmart и HeightUserSmart оба нуждаются в доступе к свойству Name, мы должны объявить его как в IThingWithHeight, так и в IThingWithVolume. И поэтому мы должны повторно объявить его в подинтерфейсе ICuboidSmart, иначе мы получим ошибку компилятора, жалующуюся на двусмысленность. В этом случае мы скрываем две версии Name, определенные в IThingWithHeight и IThingWithVolume, которые в противном случае столкнулись бы. И, как указывают другие ответы, хотя мы не должны использовать здесь new, мы должны явно указать наши намерения скрыться.

person Colm Bhandal    schedule 04.06.2020

Я использую его почти в каждом моем интерфейсе. Смотри сюда:

interface ICreature
{
    /// <summary>
    ///     Creature's age
    /// </summary>
    /// <returns>
    ///     From 0 to int.Max
    /// </returns>
    int GetAge();
}

interface IHuman : ICreature
{
    /// <summary>
    ///     Human's age
    /// </summary>
    /// <returns>
    ///     From 0 to 999
    /// </returns>
    new int GetAge();
}

Абсолютно нормально иметь унаследованного члена с меньшими требованиями, но большими обязательствами. LSP не нарушается. Но если да, то где оформить новый контракт?

person astef    schedule 17.05.2014
comment
Не уверен, что в этом случае создание метода new является наиболее подходящим способом. Для меня разный диапазон возрастов должен управляться внутренне реализациями, каждая из которых будет документировать свои инварианты. И если я продолжу ваш пример, не все люди имеют одинаковый диапазон: у патриархов действительно диапазон 0-999, но с тех пор диапазон больше 0-149. Как вы говорите, этот тип контракта не может быть описан самим языком или средой выполнения, но вы можете использовать метаданные, такие как System.ComponentModel.DataAnnotations.RangeAttribute. - person Pragmateek; 18.05.2014
comment
Дело не в диапазонах, а в требованиях и обязательствах. Диапазоны — это лишь самые простые примеры требований. Управление этими req/obl в реализациях приводит к дублированию/неповторному использованию кода. В таком простом примере это неочевидно, но неизбежно в больших проектах с глубокой иерархией. О том, что не у всех людей одинаковый диапазон - это зависит от вашего домена. В моей вселенной абстракций - это правда. В твоем - сделай свою абстракцию ;) - person astef; 18.05.2014
comment
Да, но не все требования должны быть выражены таким образом. Возьмем другой пример: если у меня есть интерфейс Vehicle с методом getNumberOfWheels, нет причин переопределять новый метод, потому что у велосипеда их 2, а у машины 4. Переопределение нового метода важно, если они делают что-то функционально отличающееся. - person Pragmateek; 18.05.2014
comment
Да, в случае IBike нет причин переопределять этот метод, если вы можете без проблем жить с широким контрактом IVehicle. В любом случае, вы реализуете этот метод в class Bike : IBike, где вы можете указать новые, более строгие обязательства, потому что каждый класс также имеет свой собственный контракт. - person astef; 19.05.2014
comment
Этот пример не имеет смысла. Интерфейс IHuman ничего не делает для обеспечения более строгих требований. На самом деле, если вы удалите new int GetAge() из IHuman, для кода не будет никаких последствий. Этот класс будет одинаковым в любом случае: public class Human : IHuman { public int GetAge() { return 0; } } - person Dru Steeby; 26.11.2019
comment
@DruSteeby Я согласен, что пример в данном случае не имеет смысла, потому что дерево наследования — это всего лишь одна строка. Но я не уверен, что код имеет нулевые последствия. Скрывая метод базового интерфейса, подинтерфейс позволяет классам реализовывать метод для двух интерфейсов по отдельности. Хотя практически это никогда не то, что вы хотите (иначе просто называйте их по-разному), я считаю, что это тонкий момент. - person Colm Bhandal; 28.05.2020