Чем отличается типизация утки от старого «варианта» и / или интерфейсов? - PullRequest
44 голосов
/ 14 ноября 2008

Я постоянно вижу фразу «типизирование утки» и даже сталкиваюсь с одним или двумя примерами кода. Я слишком ленивый занят, чтобы провести собственное исследование, может кто-нибудь сказать мне, кратко:

  • разница между 'типом утки' и 'вариантным типом' старой школы, и
  • приведите пример того, где я мог бы предпочесть тип утки, а не вариант, и
  • приведите пример чего-то, что у меня будет , чтобы использовать утки, чтобы выполнить?

duck typing illustration courtesy of The Register

Я не хочу казаться птенцом, сомневаясь в силе этой «новой» конструкции, и я не уклоняюсь от этой проблемы, отказываясь проводить исследования, но я пытаюсь справиться со всей шумихой, которую я имею видел об этом в последнее время. Мне кажется, нет печатать (иначе говоря, динамическая печать), поэтому я не вижу преимуществ сразу.

ADDENDUM: Спасибо за примеры. Мне кажется, что использование чего-то вроде 'O-> can (Blah)' эквивалентно выполнению поиска отражения (что, вероятно, недешево) и / или примерно то же самое, что сказать (O - IBlah), что может сделать компилятор Я могу проверить вас, но последний имеет преимущество, заключающееся в том, что мой интерфейс IBlah отличается от вашего интерфейса IBlah, а два других - нет. Конечно, наличие множества крошечных интерфейсов, плавающих вокруг для каждого метода, может привести к путанице, но, опять же, можно проверить множество отдельных методов ...

... так что опять я просто не понимаю. Это фантастическая экономия времени или та же самая старая вещь в новом мешке? Где пример, для которого требуется печать утки?

Ответы [ 10 ]

32 голосов
/ 14 ноября 2008

В некоторых ответах здесь я видел неправильное использование терминологии, из-за которого люди давали неправильные ответы.

Итак, прежде чем дать ответ, я приведу несколько определений:

  1. Сильно набрано

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

    Большинство языков имеют свойство "progress". Однако многие не удовлетворяют свойству «сохранения». Хорошим примером является C ++ (и C тоже). Например, в C ++ возможно заставить любой адрес памяти вести себя так, как если бы он был любого типа. Это в основном позволяет программистам нарушать систему типов в любое время. Вот простой пример:

    struct foo
    {
        int x;
        iny y;
        int z;
    }
    
    char * x = new char[100];
    foo * pFoo = (foo *)x;
    foo aRealFoo;
    *pFoo = aRealFoo;
    

    Этот код позволяет кому-то взять массив символов и записать в него экземпляр "foo". Если бы C ++ был строго типизирован, это было бы невозможно. Безопасные языки типа, такие как C #, Java, VB, lisp, ruby, python и многие другие, могут вызвать исключение, если вы попытаетесь привести массив символов к экземпляру "foo".

  2. Слабый набор

    Что-то слабо набрано, если оно не сильно набрано.

  3. Статически набрано

    Язык статически типизирован, если его система типов проверена во время компиляции. Статически типизированный язык может быть «слабо типизированным», например, C, или строго типизированным, например, C #.

  4. Динамически набранный

    Динамически типизированный язык - это язык, где типы проверяются во время выполнения. Во многих языках есть какая-то смесь между статической и динамической типизацией. C #, например, будет динамически проверять многие приведения во время выполнения, потому что невозможно проверить их во время компиляции. Другими примерами являются такие языки, как Java, VB и Objective-C.

    Есть также некоторые языки, которые «полностью» или «в основном» динамически типизированы, например, «lisp», «ruby» и «small talk»

  5. Утка набрав

    Утиная печать - это нечто, полностью ортогональное статической, динамической, слабой или строгой типизации. Это практика написания кода, который будет работать с объектом независимо от его базовой идентичности типа. Например, следующий код VB.NET:

    function Foo(x as object) as object
        return x.Quack()
    end function
    

    Будет работать независимо от типа объекта, который передается в «Foo», при условии, что он определяет метод, называемый «Кряк». То есть, если объект выглядит как утка, ходит как утка и говорит как утка, то это утка. Печатание утки приходит во многих формах. Возможно иметь статическую типизацию утки, динамическую типизацию, строгую типизацию и недельную типизацию. Шаблонные функции C ++ являются хорошим примером «слабой статической типизации утки». Пример, показанный в посте "JaredPar", показывает пример "сильной статической типизации утки". Позднее связывание в VB (или код на Ruby или Python) обеспечивает «сильную динамическую типизацию утки».

  6. Вариант

    Вариант - это динамически типизированная структура данных, которая может содержать диапазон предварительно определенных типов данных, включая строки, целочисленные типы, даты и com-объекты. Затем он определяет набор операций для назначения, преобразования и манипулирования данными, хранящимися в вариантах. Является ли вариант строго типизированным, зависит от языка, на котором он используется. Например, вариант в программе VB 6 строго типизирован. Среда выполнения VB гарантирует, что операции, написанные в коде VB, будут соответствовать правилам ввода для вариантов. Попытка добавить строку в IUnknown через тип варианта в VB приведет к ошибке во время выполнения. Однако в C ++ варианты слабо типизированы, поскольку все типы C ++ слабо типизированы.

ОК .... теперь, когда я получил определения, я теперь могу ответить на ваш вопрос:

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

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

18 голосов
/ 14 ноября 2008

Простой ответ - вариант слабо набран, а утка строго набрана.

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

interface IDuck {
  void Quack();
}

Теперь давайте рассмотрим Даффи

class Daffy {
  void Quack() {
    Console.WriteLine("Thatsssss dispicable!!!!");
  }
}

Даффи в данном случае на самом деле не идук. Тем не менее, он действует как утка. Зачем заставлять Даффи реализовывать IDuck, когда совершенно очевидно, что Даффи на самом деле утка?

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

IDuck d = new Daffy();
d.Quack();

Метод Quack теперь можно вызывать на «d» с полной безопасностью типов. В этом присваивании или вызове метода нет шансов на ошибку типа времени выполнения.

5 голосов
/ 14 ноября 2008

Утиная печать - это еще один термин для динамической типизации или позднего связывания. Вариантный объект, который анализирует / компилирует любой доступ к элементу (например, obj.Anything), который может или не может быть фактически определен во время выполнения, - это утка.

4 голосов
/ 16 ноября 2008

Вероятно, ничего не требует утки, но в определенных ситуациях это может быть удобно. Допустим, у вас есть метод, который берет и использует объект запечатанного класса Duck из какой-то сторонней библиотеки. И вы хотите сделать метод тестируемым. А в Duck есть очень большой API (вроде ServletRequest), о котором вам нужно заботиться только о небольшом подмножестве. Как вы это тестируете?

Один из способов - заставить метод взять что-то крякающее. Тогда вы можете просто создать поддельный объект.

3 голосов
/ 14 ноября 2008

Попробуйте прочитать самый первый абзац статьи в Википедии о наборе утки.
Утиная печать в Википедии

У меня может быть интерфейс (IRunnable), который определяет метод Run ().
Если у меня есть другой класс с методом, подобным этому:
public void RunSomeRunnable (IRunnable rn) {...}

На языке, удобном для утиного типа, я мог передать любой класс, имеющий метод Run (), в метод RunSomeRunnable ().
В статически типизированном языке класс, передаваемый в RunSomeRunnable, должен явно реализовывать интерфейс IRunnable.

«Если он бежит () как утка»

вариант больше похож на объект в .NET, по крайней мере.

2 голосов
/ 16 ноября 2008

Что касается вашего запроса на пример чего-то, что вам понадобится , чтобы выполнить набор по утке, я не думаю, что такая вещь существует. Я думаю об этом, как о том, использовать ли рекурсию или использовать итерацию. Иногда одно работает лучше другого.

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

Я говорю, что если вам удобно использовать статическую типизацию, используйте ее. Но вы должны, по крайней мере, попытаться ввести утку (и использовать ее в нетривиальном проекте, если это возможно).

Чтобы ответить вам более прямо:

... так что опять я просто не понимаю. Это фантастическая экономия времени или та же самая старая вещь в новом мешке?

Это оба. Вы все еще атакуете те же проблемы. Вы просто делаете это по-другому. Иногда это действительно все, что вам нужно сделать, чтобы сэкономить время (даже если по какой-либо другой причине вынуждаете себя думать о чем-то другом).

Является ли это панацеей, которая спасет все человечество от вымирания? Нет. И любой, кто говорит тебе иначе, является фанатиком.

2 голосов
/ 14 ноября 2008

@ Кент Фредрик

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

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

public interface ICreature { }
public interface IFly { fly();}
public interface IWalk { walk(); }
public interface IQuack { quack(); }
// ETC

// Animal Class
public class Duck : ICreature, IWalk, IFly, IQuack
{
    fly() {};
    walk() {};
    quack() {};
}

public class Rhino: ICreature, IWalk
{
    walk();
}

// In the method
List<ICreature> creatures = new List<ICreature>();
creatures.Add(new Duck());
creatures.Add(new Rhino());   

foreach (ICreature creature in creatures)
{
    if (creature is IFly)        
         (creature as IFly).fly();        
    if (creature is IWalk) 
         (creature as IWalk).walk();         
}
// Etc
1 голос
/ 14 ноября 2008

Вариант (по крайней мере, как я их использовал в VB6) содержит переменную одного четко определенного, обычно статического типа. Например, он может содержать int, или float, или строку, но альтернативные int используются как int, вариативные float используются как float, а альтернативные строки используются как строки.

Утиная печать вместо этого использует динамическую печать. При вводе данных утилитой переменная может использоваться как int, или float, или как строка, если она поддерживает определенные методы, которые int, float или string поддерживают в определенном контексте.

Пример варианта против набора утки:

Предположим, что для веб-приложения я хочу, чтобы моя пользовательская информация поступала из LDAP, а не из базы данных, но я все же хочу, чтобы моя пользовательская информация использовалась остальной частью веб-инфраструктуры, основанной на базе данных и ОРМ.

Использование вариантов: не повезло. Я могу создать вариант, который может содержать объект UserFromDbRecord или объект UserFromLdap, но объекты UserFromLdap не будут использоваться подпрограммами, которые ожидают объекты из иерархии FromDbRecord.

Использование утилитной типизации: я могу взять свой класс UserFromLdap и добавить несколько методов, которые заставят его действовать как класс UserFromDbRecord. Мне не нужно копировать весь интерфейс FromDbRecord, достаточно только для подпрограмм, которые мне нужно использовать. Если я делаю это правильно, это чрезвычайно мощный и гибкий метод. Если я сделаю это неправильно, он выдаст очень запутанный и хрупкий код (может быть поврежден, если изменится либо библиотека БД, либо библиотека LDAP).

1 голос
/ 14 ноября 2008

Я думаю, что суть утиной печати заключается в том, как она используется. Один использует метод обнаружения и самоанализа сущности для того, чтобы узнать, что с ним делать, вместо того, чтобы заранее объявлять, чем он будет * (где вы знаете, что с ним делать).

Вероятно, это более практично в ОО-языках, где примитивы не являются примитивами, а являются объектами.

Я думаю, что лучший способ подвести итог, в варианте типа, сущность есть / может быть чем угодно, и что это неопределенно, в отличие от сущности только выглядит как что-либо, но вы можно выяснить, что это, спросив об этом.

Вот кое-что, что я не верю, правдоподобно без утки.

sub dance { 
     my $creature = shift;
     if( $creature->can("walk") ){ 
         $creature->walk("left",1);
         $creature->walk("right",1); 
         $creature->walk("forward",1);
         $creature->walk("back",1);
     }
     if( $creature->can("fly") ){ 
          $creature->fly("up"); 
          $creature->fly("right",1); 
          $creature->fly("forward",1); 
          $creature->fly("left", 1 ); 
          $creature->fly("back", 1 ); 
          $creature->fly("down");
     } else if ( $creature->can("walk") ) { 
         $creature->walk("left",1);
         $creature->walk("right",1); 
         $creature->walk("forward",1);
         $creature->walk("back",1);
     } else if ( $creature->can("splash") ) { 
         $creature->splash( "up" ) for ( 0 .. 4 ); 
     }
     if( $creature->can("quack") ) { 
         $creature->quack();
     }
 }

 my @x = ();  
 push @x, new Rhinoceros ; 
 push @x, new Flamingo; 
 push @x, new Hyena; 
 push @x, new Dolphin; 
 push @x, new Duck;

 for my $creature (@x){

    new Thread(sub{ 
       dance( $creature ); 
    }); 
 }

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

И это действительно отстой с точки зрения просто попытки выполнить хорошую хореографию.

0 голосов
/ 12 февраля 2010

Все, что вы можете сделать с помощью утки, вы можете сделать и с интерфейсами. Утиная печать быстрая и удобная, но некоторые утверждают, что это может привести к ошибкам (если два разных метода / свойства названы одинаково) Интерфейсы безопасны и понятны, но люди могут сказать: «Почему утверждают очевидное?». Отдых это пламя. Каждый выбирает то, что ему подходит, и никто не «прав».

...