C # інтерфейси. Неявна реалізація проти явної реалізації


632

Які відмінності в реалізації інтерфейсів неявно і явно в C #?

Коли слід використовувати неявно і коли слід використовувати явне?

Чи є плюси та / або мінуси одних чи інших?


В офіційних рекомендаціях Microsoft (від першої редакції Framework Design Guidelines ) зазначено, що використання явних реалізацій не рекомендується , оскільки це дає коду несподівану поведінку.

Я думаю, що ця інструкція є дуже дійсною до часу , коли ви не передаєте речі як інтерфейси.

Чи може хтось торкнутися і цього аспекту?


Читайте повну статтю про інтерфейси C #: planetofcoders.com/c-interfaces
Gaurav Agrawal

Так, явних інтерфейсів слід уникати, і більш професійний підхід би застосувати ISP (принцип сегрегації інтерфейсу). Ось детальна стаття на той же codeproject.com/Articles/1000374/…
Shivprasad Koirala

Відповіді:


492

Імпліцит - це коли ви визначаєте свій інтерфейс через учасника свого класу. Явне - це коли ви визначаєте в інтерфейсі методи свого класу. Я знаю, що це звучить заплутано, але ось що я маю на увазі: IList.CopyToбуде неявно реалізовано як:

public void CopyTo(Array array, int index)
{
    throw new NotImplementedException();
}

і явно як:

void ICollection.CopyTo(Array array, int index)
{
    throw new NotImplementedException();
}

Різниця полягає в тому, що неявна реалізація дозволяє отримати доступ до інтерфейсу через створений вами клас, передаваючи інтерфейс як цей клас, так і як сам інтерфейс. Явна реалізація дозволяє отримати доступ до інтерфейсу лише шляхом передачі його як самого інтерфейсу.

MyClass myClass = new MyClass(); // Declared as concrete class
myclass.CopyTo //invalid with explicit
((IList)myClass).CopyTo //valid with explicit.

Явно використовую явні функції, щоб забезпечити чистоту реалізації чи коли мені потрібні дві реалізації. Незважаючи на те, я його рідко використовую.

Я впевнений, що є більше причин використовувати / не використовувати явні, які публікуватимуть інші.

Дивіться наступну публікацію в цій темі, щоб отримати відмінні міркування за кожним.


8
Я знаю, що ця публікація стара, але я вважаю її дуже корисною - одне зауважити, якщо вона не зрозуміла, тому що не для мене було те, що в прикладі неявна одна з них має publicключове слово ... інакше ви отримаєте помилку
jharr100

CLR Джеффрі Ріхтера через C # 4 ed ch 13 показує приклад, коли кастинг не потрібен: внутрішня структура SomeValueType: IComparable {private Int32 m_x; public SomeValueType (Int32 x) {m_x = x; } публічний Int32 CompareTo (SomeValueType інший) {...);} Int32 IComparable.CompareTo (Object other) {повернути CompareTo ((SomeValueType) інше); }} публічний статичний недійсний Main () {SomeValueType v = новий SomeValueType (0); Об'єкт o = новий Об'єкт (); Int32 n = v.CompareTo (v); // Без боксу n = v.CompareTo (o); // Помилка часу компіляції}
Енді Дент

1
Сьогодні я зіткнувся з рідкісною ситуацією, яка ПОТРІБНО використовувала явний інтерфейс: Клас із полем, породженим конструктором інтерфейсів, який створює поле як приватне (Xamarin, націлений на iOS, за допомогою iOS раскадровки). І інтерфейс, де було сенс розкривати це поле (публічне лише для читання). Я міг змінити ім'я геттера в інтерфейсі, але існуюче ім'я було найбільш логічним ім'ям для об'єкта. Так що замість цього я зробив явну реалізацію зі посиланням на приватне поле: UISwitch IScoreRegPlayerViewCell.markerSwitch { get { return markerSwitch; } }.
ToolmakerSteve

1
Жахи програмування. Ну, приємно розвінчав!
Рідке ядро

1
@ToolmakerSteve Інша ситуація (досить частіша), яка вимагає явної реалізації принаймні одного учасника інтерфейсу, реалізує декілька інтерфейсів, у яких є члени з однаковою підписом, але різні типи повернення. Це може статися з - за успадкування інтерфейсів, як це відбувається з IEnumerator<T>.Current, IEnumerable<T>.GetEnumerator()і ISet<T>.Add(T). Про це йдеться в іншій відповіді .
фог

201

Неявним визначенням буде просто додавання методів / властивостей тощо, що вимагаються інтерфейсом безпосередньо до класу як публічних методів.

Явне визначення змушує членів піддаватися впливу лише тоді, коли ви безпосередньо працюєте з інтерфейсом, а не з базовою реалізацією. Це є переважним у більшості випадків.

  1. Працюючи безпосередньо з інтерфейсом, ви не підтверджуєте і приєднуєте свій код до базової реалізації.
  2. У випадку, якщо у вашому коді вже є, скажімо, ім'я загальнодоступної власності, і ви хочете реалізувати інтерфейс, який також має властивість Name, явно це зробить ці два окремими. Навіть якби вони робили те саме, я все-таки делегував би явний виклик властивості Name. Ніколи не знаєш, можливо, ти захочеш змінити, як Ім'я працює для нормального класу та як Ім'я, властивість інтерфейсу працює згодом.
  3. Якщо ви реалізуєте інтерфейс неявно, то ваш клас тепер виявляє нову поведінку, яка може стосуватися лише клієнта інтерфейсу, і це означає, що ви не тримаєте свої класи достатньо лаконічними (на мою думку).

2
Ви заробляєте тут кілька хороших моментів. особливо А. Я зазвичай передаю свої класи як інтерфейс, але я ніколи не задумувався над цією перспективою.
матлант

5
Я не впевнений, що згоден з пунктом C. Об'єкт Cat може реалізувати IEatable, але Eat () - це основна частина речі. Були б випадки, коли ви хочете просто зателефонувати Eat () на Cat, коли ви використовуєте "сирий" об'єкт, а не через інтерфейс IEatable, ні?
LegendLength

59
Я знаю про деякі місця, де Catсправді IEatableбез заперечень.
Умберто

26
Я повністю не погоджуюся з усім вищесказаним і можу сказати, що використання явних інтерфейсів є рецептом катастрофи, а не OOP чи OOD за їх визначенням (див. Мою відповідь про поліморфізм)
Валентин Кузуб,

2
Дивіться також: stackoverflow.com/questions/4103300/…
Zack Jannsen

68

Окрім відмінних відповідей, що вже надаються, є деякі випадки, коли ПОТРІБНА чітка реалізація, щоб компілятор міг зрозуміти, що потрібно. Погляньте на це IEnumerable<T>як на головний приклад, який, швидше за все, з’явиться досить часто.

Ось приклад:

public abstract class StringList : IEnumerable<string>
{
    private string[] _list = new string[] {"foo", "bar", "baz"};

    // ...

    #region IEnumerable<string> Members
    public IEnumerator<string> GetEnumerator()
    {
        foreach (string s in _list)
        { yield return s; }
    }
    #endregion

    #region IEnumerable Members
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    #endregion
}

Тут IEnumerable<string>реалізація IEnumerable, отже, нам теж потрібно. Але затримайтеся, і загальна, і звичайна версія обидва реалізують функції з однаковим підписом методу (C # ігнорує тип повернення для цього). Це цілком законно і добре. Як компілятор вирішує, який використовувати? Це змушує вас мати, максимум, одне неявне визначення, тоді воно може вирішити все, що потрібно.

тобто.

StringList sl = new StringList();

// uses the implicit definition.
IEnumerator<string> enumerableString = sl.GetEnumerator();
// same as above, only a little more explicit.
IEnumerator<string> enumerableString2 = ((IEnumerable<string>)sl).GetEnumerator();
// returns the same as above, but via the explicit definition
IEnumerator enumerableStuff = ((IEnumerable)sl).GetEnumerator();

PS: Маленький фрагмент непрямості в явному визначенні для IEnumerable працює, тому що всередині функції компілятор знає, що фактичний тип змінної є StringList, і саме тому вона вирішує виклик функції. Невеликий факт, який реалізує деякі шари абстракції, деякі інтерфейси .NET core, здається, накопичилися.


5
@Tassadaque: Через 2 роки все, що я можу сказати, - це "гарне питання". Я поняття не маю, крім, мабуть, того, що я скопіював цей код з чогось, над чим працював abstract.
Меттью Шарлі

4
@Tassadaque, ви маєте рацію, але я вважаю, що точка поста Меттью Шарлі вище не втрачена.
funkymushroom

35

Цитувати Джефрі Ріхтера з CLR через C #
( EIMI означає E xplicit I nterface M ethod I mplementation)

Для вас критично важливо зрозуміти деякі наслідки, які існують при використанні EIMI. І через ці наслідки, ви повинні намагатися максимально уникати EIMI. На щастя, загальні інтерфейси допомагають вам зовсім уникнути EIMI. Але все ж можуть бути випадки, коли вам доведеться їх використовувати (наприклад, реалізувати два методи інтерфейсу з однаковим іменем та підписом). Ось великі проблеми з EIMI:

  • Немає документації, що пояснює, як тип спеціально реалізує метод EIMI, і немає підтримки Microsoft Visual Studio IntelliSense .
  • Екземпляри типу значень відображаються у вікні при передачі інтерфейсу.
  • EIMI не може бути викликаний похідним типом.

Якщо ви використовуєте посилання на інтерфейс, будь-яка віртуальна ланцюг може бути явно замінена EIMI на будь-якому похідному класі, і коли об'єкт такого типу передається в інтерфейс, ваша віртуальна ланцюг ігнорується і викликається явна реалізація. Це все, крім поліморфізму.

EIMI можуть також використовуватися для приховування не сильно набраних членів інтерфейсу від базових реалізацій базових інтерфейсів, таких як IEnumerable <T>, тому ваш клас не піддає безпосередньо сильно набраному методу, але є синтаксично правильним.


2
Повторна реалізація інтерфейсів, хоч і легальна, як правило, в кращому випадку сумнівна. Явні реалізації зазвичай мають поширюватися на віртуальний метод або безпосередньо, або через обробну логіку, яка повинна бути обов'язковою для похідних класів. Хоча, безумовно, можливо використовувати інтерфейси способами, ворожими правильним конвенціям OOP, це не означає, що їх не можна використовувати краще.
supercat

4
@Valentin Що означають EIMI та IEMI?
Dzienny

Реалізація
явного

5
-1 для "Взагалі інтерфейси я бачу як напів (в кращому випадку) функцію OOP, вона забезпечує спадщину, але не забезпечує справжній поліморфізм." Я категорично не згоден. Зовсім навпаки, інтерфейси - це все про поліморфізм і, перш за все, не про спадкування. Вони приписують декілька класифікацій типу. Уникайте IEMI, коли можете, і делегуйте, як @supercat запропонував, коли не можете. Не уникайте інтерфейсів.
Алуан Хаддад

1
"EIMI не може бути викликаний похідним типом." << ЩО? Це не правда. Якщо я явно реалізую інтерфейс для типу, то я виходжу з цього типу, я все-таки можу передавати його в інтерфейс, щоб викликати метод, точно так само, як і для типу, на якому він реалізований. Тож не впевнений, про що ти говориш. Навіть у похідному типі я можу просто кинути "це" на відповідний інтерфейс, щоб досягти явно реалізованого методу.
Трайнко

34

Причина №1

Я схильний використовувати явну реалізацію інтерфейсу, коли хочу відмовити "програмуванню до реалізації" ( Принципи проектування від шаблонів дизайну ).

Наприклад, у веб-додатку на базі MVP :

public interface INavigator {
    void Redirect(string url);
}

public sealed class StandardNavigator : INavigator {
    void INavigator.Redirect(string url) {
        Response.Redirect(url);
    }
}

Тепер інший клас (наприклад, презентатор ) менше шансів залежати від реалізації StandardNavigator і скоріше залежатиме від інтерфейсу INavigator (оскільки реалізація повинна бути передана в інтерфейс, щоб використовувати метод перенаправлення).

Причина №2

Ще одна причина, з якою я можу піти з явною реалізацією інтерфейсу, - це збереження інтерфейсу класу «за замовчуванням». Наприклад, якщо я розробляв керування сервером ASP.NET , я, можливо, захотів би два інтерфейси:

  1. Основний інтерфейс класу, який використовується розробниками веб-сторінок; і
  2. "Прихований" інтерфейс, використовуваний презентатором, який я розробляю для обробки логіки управління

З цього випливає простий приклад. Це комбінований елемент управління, який перераховує клієнтів. У цьому прикладі розробник веб-сторінки не зацікавлений у заповненні списку; натомість вони просто хочуть мати можливість вибрати клієнта за GUID або отримати GUID вибраного клієнта. Презентатор заповнить поле під час завантаження першої сторінки, і цей ведучий інкапсульований елементом управління.

public sealed class CustomerComboBox : ComboBox, ICustomerComboBox {
    private readonly CustomerComboBoxPresenter presenter;

    public CustomerComboBox() {
        presenter = new CustomerComboBoxPresenter(this);
    }

    protected override void OnLoad() {
        if (!Page.IsPostBack) presenter.HandleFirstLoad();
    }

    // Primary interface used by web page developers
    public Guid ClientId {
        get { return new Guid(SelectedItem.Value); }
        set { SelectedItem.Value = value.ToString(); }
    }

    // "Hidden" interface used by presenter
    IEnumerable<CustomerDto> ICustomerComboBox.DataSource { set; }
}

Презентатор заповнює джерело даних, а розробник веб-сторінок ніколи не повинен знати про його існування.

Але це не срібляста пушка

Я б не рекомендував завжди використовувати явні реалізації інтерфейсу. Це лише два приклади, коли вони можуть бути корисними.


19

Окрім інших, вже зазначених причин, це ситуація, коли клас реалізує два різних інтерфейси, які мають властивість / метод з тим самим ім'ям та підписом.

/// <summary>
/// This is a Book
/// </summary>
interface IBook
{
    string Title { get; }
    string ISBN { get; }
}

/// <summary>
/// This is a Person
/// </summary>
interface IPerson
{
    string Title { get; }
    string Forename { get; }
    string Surname { get; }
}

/// <summary>
/// This is some freaky book-person.
/// </summary>
class Class1 : IBook, IPerson
{
    /// <summary>
    /// This method is shared by both Book and Person
    /// </summary>
    public string Title
    {
        get
        {
            string personTitle = "Mr";
            string bookTitle = "The Hitchhikers Guide to the Galaxy";

            // What do we do here?
            return null;
        }
    }

    #region IPerson Members

    public string Forename
    {
        get { return "Lee"; }
    }

    public string Surname
    {
        get { return "Oades"; }
    }

    #endregion

    #region IBook Members

    public string ISBN
    {
        get { return "1-904048-46-3"; }
    }

    #endregion
}

Цей код компілюється та працює ОК, але властивість Title поділяється.

Зрозуміло, ми хотіли б, щоб значення заголовка повернулося залежно від того, ми ставимось до класу1 як до книги чи до особистості. Це коли ми можемо використовувати явний інтерфейс.

string IBook.Title
{
    get
    {
        return "The Hitchhikers Guide to the Galaxy";
    }
}

string IPerson.Title
{
    get
    {
        return "Mr";
    }
}

public string Title
{
    get { return "Still shared"; }
}

Зауважте, що явні визначення інтерфейсу вважаються загальнодоступними, а значить, ви не можете оголосити їх публічними (або іншим чином) явно.

Зауважте також, що ви все ще можете мати "спільну" версію (як показано вище), але, хоча це можливо, існування такої властивості сумнівне. Можливо, він може використовуватися як реалізація титулу за замовчуванням - так що існуючий код не повинен був би бути змінений, щоб передати Class1 на IBook або IPerson.

Якщо ви не визначите "загальний" (неявний) заголовок, споживачі Class1 повинні спочатку явно передати екземпляри Class1 в IBook або IPerson - інакше код не складеться.


16

Я використовую явну реалізацію інтерфейсу більшу частину часу. Ось основні причини.

Рефакторинг безпечніший

При зміні інтерфейсу краще, якщо компілятор може це перевірити. Це складніше з неявними реалізаціями.

На думку приходять два поширені випадки:

  • Додавання функції до інтерфейсу, коли вже наявний клас, який реалізує цей інтерфейс, має метод із тим же підписом, що і новий . Це може призвести до несподіваної поведінки, і мене кілька разів сильно вкусив. Важко "побачити" під час налагодження, оскільки ця функція, ймовірно, не розташована з іншими методами інтерфейсу у файлі (проблема самодокументування, згадана нижче).

  • Видалення функції з інтерфейсу . Неявно реалізовані методи будуть раптово мертвим кодом, але явно реалізовані методи потраплять через помилку компіляції. Навіть якщо мертвий код добре зберігати, я хочу примусити його переглянути та просувати.

Прикро, що у C # немає ключового слова, яке змушує нас позначати метод як неявну реалізацію, тому компілятор міг зробити додаткові перевірки. У віртуальних методах немає жодної з перерахованих вище проблем через необхідне використання "переопределення" та "нового".

Примітка: для фіксованих або рідко мінливих інтерфейсів (як правило, від API постачальника) це не проблема. Для власних інтерфейсів, однак, я не можу передбачити, коли / як вони зміняться.

Це самодокументація

Якщо я бачу "public bool Execute ()" у класі, знадобиться додаткова робота, щоб з'ясувати, що це частина інтерфейсу. Комусь, мабуть, доведеться прокоментувати це, сказавши так, або помістити його в групу інших реалізацій інтерфейсу, все під регіоном чи групуючим коментарем, що говорить "реалізація ITask". Звичайно, це працює лише в тому випадку, якщо заголовок групи не є екраном.

Тоді як: "bool ITask.Execute ()" є зрозумілим і однозначним.

Чітке розділення реалізації інтерфейсу

Я вважаю, що інтерфейси є більш «публічними», ніж публічні методи, оскільки вони створені, щоб відкрити лише трохи поверхні бетонного типу. Вони зводять тип до працездатності, поведінки, набору рис тощо. І при здійсненні, я думаю, корисно зберегти це розмежування.

Переглядаючи код класу, коли я стикаюся з явними реалізаціями інтерфейсу, мій мозок переходить у режим «кодового контракту». Часто ці реалізації просто переадресують інші методи, але іноді вони здійснюватимуть додаткову перевірку стану / парам, перетворення вхідних параметрів, щоб краще відповідати внутрішнім вимогам, або навіть перекладати для цілей версії (тобто декілька поколінь інтерфейсів, які все зводяться до загальних реалізацій).

(Я усвідомлюю, що паблік - це також кодові контракти, але інтерфейси набагато сильніші, особливо в інтерфейсі, котрий керується інтерфейсом, де пряме використання конкретних типів зазвичай є ознакою коду лише для внутрішніх справ.)

Пов'язане: Причина 2 вище Джон .

І так далі

Плюс переваги, про які вже говорилося в інших відповідях тут:

Проблеми

Це не все весело і щастя. Є деякі випадки, коли я дотримуюся імпліцитів:

  • Типи цінності, тому що для цього знадобиться бокс і нижча perf. Це не суворе правило, і залежить від інтерфейсу та способу його використання. Ізрівнянний? Неявне. ІФОРМАТИВНО? Напевно, явний.
  • Тривіальні системні інтерфейси, у яких є методи, які часто викликаються безпосередньо (наприклад, IDisposable.Dispose).

Крім того, може бути болісно робити кастинг, якщо ви насправді маєте конкретний тип і хочете викликати явний метод інтерфейсу. Я маю справу з цим одним із двох способів:

  1. Додайте публікації та запропонуйте їм методи інтерфейсу для впровадження. Зазвичай це відбувається з більш простими інтерфейсами при роботі всередині.
  2. (Мій кращий метод) Додайте public IMyInterface I { get { return this; } }(який має бути вбудованим) та зателефонуйте foo.I.InterfaceMethod(). Якщо декілька інтерфейсів, які потребують цієї здатності, розгорніть ім’я за межами I (на моєму досвіді, у мене є рідкість, що я маю цю потребу).

8

Якщо явно реалізувати, ви зможете посилатись на членів інтерфейсу лише через посилання, яке є типом інтерфейсу. Посилання, що є типом класу реалізації, не піддаватиме дії цих членів інтерфейсу.

Якщо ваш клас реалізації не є загальнодоступним, за винятком методу, який використовується для створення класу (який може бути заводським або IoC- контейнером), і за винятком методів інтерфейсу (звичайно), я не бачу жодної переваги в явній реалізації інтерфейси.

В іншому випадку явна реалізація інтерфейсів гарантує, що посилання на ваш конкретний клас реалізації не використовуються, що дозволяє змінити цю реалізацію пізніше. «Має впевненість», я гадаю, є «перевагою». Добре реалізована реалізація може досягти цього без явної реалізації.

Недоліком, на мою думку, є те, що у коді реалізації ви знайдете типові типи в / з інтерфейсу, які мають доступ до непублічних членів.

Як і багато речей, перевагою є недолік (і навпаки). Явно реалізовані інтерфейси забезпечать, щоб ваш код реалізації конкретного класу не піддавався.


1
Гарна відповідь, Білл. Інші відповіді були чудовими, але ви надали додаткові об'єктивні точки зору (на вершині вашої думки), що полегшило мені розуміння. Як і в більшості речей, є плюси і мінуси з неявними або явними реалізаціями, тому вам просто потрібно використовувати найкращий для вашого конкретного сценарію або використання. Я б сказав, що ті, хто намагається зрозуміти це, отримають користь від прочитання вашої відповіді.
Даніель Орел

6

Неявна реалізація інтерфейсу - це те, де у вас є метод з однаковою підписом інтерфейсу.

Явна реалізація інтерфейсу - це ви явно заявляєте, до якого інтерфейсу належить метод.

interface I1
{
    void implicitExample();
}

interface I2
{
    void explicitExample();
}


class C : I1, I2
{
    void implicitExample()
    {
        Console.WriteLine("I1.implicitExample()");
    }


    void I2.explicitExample()
    {
        Console.WriteLine("I2.explicitExample()");
    }
}

MSDN: неявна та явна реалізація інтерфейсу


6

Кожен член класу, який реалізує інтерфейс, експортує декларацію, семантично схожу на те, як написано декларації інтерфейсу VB.NET, наприклад

Public Overridable Function Foo() As Integer Implements IFoo.Foo

Хоча ім’я учасника класу часто збігається з ім'ям учасника інтерфейсу, а член класу часто є загальнодоступним, нічого з цього не потрібно. Можна також заявити:

Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo

У такому випадку клас та його похідні дозволяють отримати доступ до члена класу з використанням імені IFoo_Foo, але зовнішній світ матиме змогу отримати доступ до цього конкретного члена лише за допомогою кастингу IFoo. Такий підхід часто хороший у тих випадках, коли метод інтерфейсу матиме задану поведінку у всіх реалізаціях, а корисна поведінка лише для деяких (наприклад, задана поведінка для IList<T>.Addметоду колекції, що читається лише для кидання NotSupportedException). На жаль, єдиним правильним способом реалізації інтерфейсу в C # є:

int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }

Не так приємно.


2

Одне важливе використання явної реалізації інтерфейсу - це коли потрібно реалізувати інтерфейси зі змішаною видимістю .

Проблема та рішення добре пояснені у статті C # Internal Interface .

Наприклад, якщо ви хочете захистити витік об'єктів між шарами програми, ця методика дозволяє задати різні видимість елементів, які могли б викликати витік.


2

Попередні відповіді пояснюють, чому явна реалізація інтерфейсу в C # може бути кращою (здебільшого формальних причин). Однак є одна ситуація, коли явна реалізація є обов'язковою : Щоб уникнути протікання інкапсуляції, коли інтерфейс не- public, але клас реалізації є public.

// Given:
internal interface I { void M(); }

// Then explicit implementation correctly observes encapsulation of I:
// Both ((I)CExplicit).M and CExplicit.M are accessible only internally.
public class CExplicit: I { void I.M() { } }

// However, implicit implementation breaks encapsulation of I, because
// ((I)CImplicit).M is only accessible internally, while CImplicit.M is accessible publicly.
public class CImplicit: I { public void M() { } }

Вищевказаний витік неминучий, оскільки, згідно зі специфікацією C # , "усі учасники інтерфейсу неявно мають доступ до публіки". Як наслідок, неявна реалізація також повинна надавати publicдоступ, навіть якщо сам інтерфейс є internal.

Неявна реалізація інтерфейсу в C # - це велика зручність. На практиці багато програмістів користуються ним постійно / скрізь без подальшого розгляду. Це призводить до брудних поверхонь у кращому випадку і просочується капсуляцією в гіршому випадку. Інші мови, такі як F #, навіть не дозволяють .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.