Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Показаны сообщения с ярлыком programming languages. Показать все сообщения
Показаны сообщения с ярлыком programming languages. Показать все сообщения

Type system covariance and contravariance

Полагаю, многие разработчики не знают, что такое ковариантность/контравариарность системы типов. К примеру, в версии 1.5 языка Java появилась возможность использовать тип класса-наследника в объявлении замещающей функции:

 0: /**
 1:  *
 2:  */
 3: package com.chabster.covariance;
 4:
 5: /**
 6:  * @author Chabster
 7:  */
 8: public class Container
 9: {
10:   class A
11:   {
12:      A some() {
13:         return (this);
14:      }
15:   }
16:
17:   class B extends A
18:   {
19:      @Override
20:      B some() {
21:         return (this);
22:      }
23:   }
24:
25: }

В С++ такая возможность существует уже давно, а вот в C# все еще отсутствует. Полагаю, это связано с нестыковкой сигнатур методов в C# и IL. Вот, что говорит по этому поводу Standard ECMA-335 Common Language Infrastructure (CLI) 4th edition (June 2006):

A method signatures is composed of

  • a calling convention,
  • the number of generic parameters, if the method is generic,
  • a list of zero or more parameter signatures—one for each parameter of the method—and,
  • a type signature for the result value, if one is produced.

Получается, что IL код различает методы, которые отличаются лишь типом возврата, а C# - нет. Как и любой существующий на данный момент управляемый .NET-язык. С другой стороны этот недостаток можно элементарно восполнить. Вариант реализации предлогаю расценивать, как домашнее задание для читателя :).

В некоторых моментах C#, все же, поддерживает ковариантность и контравариантнось. Но для начала выясним что это такое.

Ковариантность (covariance) и контравариантнось (contravariance)

Ковариантностью называют сохранение формы при преобразовании. Соответсвенно, ковариантным называется преобразование, сохраняющее форму (свойства).

Например, оператор F(x) = x*2 является ковариантным касательно отношения % (делимость). Т.е. из x%y следует, что F(x)%F(y).

Контравариантностью называют обращение формы при преобразовании. Соответсвенно, контравариантным называется преобразование, обращающее форму.

Например, оператор F(x) = -x является контравариантным касательно отношения > (больше). Т.е. из x>y следует, что F(x)<F(y).

Оба этих понятия нельзя правильно обобщить поскольку в каждой науке оно выглядит по-своему. Рассмотрим их в самой интересной для нас области - программирование!

Ковариантность и контравариантнось в системе типов языков программирования

Рассмотрим пример C# кода:

class A {
    A[] aArr = new B[] { };
}

class B : A {
}

Пусть « обозначает отношение is a для типов. Поэтому B«A. Оператор Arrize({T}) = {T[]} является ковариантным касательно отношения is a для ссылочных типов т.к. из B«A следует B[]«A[].

Возвращаясь к первому Java примеру можно сказать, что оператор Methodize({SuperT virtMethod(T1,T2,...)}) = {SubT virtMethod(T1,T2,...)} является ковариантным касательно отношения override между методами.

Ковариантность и контравариантнось в системе типов C#

В C# есть ковариация, связанная с делегатами:

class A {
}

class B : A {
}

delegate A SomeDelegate();

static B SomeMethod() { return (null); }

static SomeDelegate sd = SomeMethod;

Типы возвращаемых значений метода и делегата различны, но совместимы.

В C# есть и контравариация, связанная с делегатами:

class A {
}

class B : A {
}

delegate void SomeDelegate(B b);

static void SomeMethod(A a) { }

static SomeDelegate sd = SomeMethod;

Сигнатуры метода и делегата различны, но совместимы, правда, уже в обратном направлении.

Это все, конечно, хорошо, вот только...

class A {
}

class B : A {
}

delegate T SomeDelegate<T>();

static B SomeMethod() { return (null); }

static SomeDelegate<B> sdB = SomeMethod;

static SomeDelegate<A> sdA = sdB;

... последняя строчка вызывает ошибку компиляции.

В целом, вариантность - очень полезный механизм, если им правильно пользоваться.

Weak Static and Strong Dynamic typing

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

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

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

О типах и типизации

Прежде чем разговаривать о типизации и ее видах необходимо разобраться с самими понятиями типа и типизации.

Тип - это некая информация о участке памяти.

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

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

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

Строгая (strong) и нестрогая (weak) типизация

Мне кажется, что эти два противоположных понятия следует объединить в одно - строгость типизации. Рассмотрим похожие фрагменты кода различных языков:

// PHP - говорят, что это язык с нестрогой типизацией
$a = "1";
$b =  2;
$c = a + b;
// $c == 3
// C# 3.0 - говорят, что это язык со строгой типизацией
String a = "1";
Int32 b =  2;
var c = a + b;
// $c == "12"

Что же я вижу здесь? Первое: переменные могут быть связаны с типом данных (C# 3.0) или не связаны (PHP). Это влияет на возможность определить тип результата или легитимность операций во время обработки исходного кода (компилятором или средой разработки). Многие утверждают, что именно это указывает на строгую или нестрогую типизацию, а я считаю, что привязанность переменных к типам языка напрямую связана с разделением на статическую и динамическую типизацию. Второе: компилятор имеет некие правила преобразования конструкций, которые вызывают нестыковку с системой типов языка или представлением разработчика о их логичности. Чем больше этих правил - тем меньше строгость типизации. Насколько мне известно, все языки имеют поддержку минимум одного неявного преобразования типов (coercion), поэтому языка с 100%-но строгой типизацией не существует.

Статическая (static) и динамическая (dynamic) типизация

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

Статическая типизация (static typing)

Рассмотрим пример на языке С++:

class CppClass
{
public:
    void SomeOperation() {}
};

void PerformOperation(CppClass &c) {
    c.SomeOperation();
}

...

CppClass c;
PerformOperation(c);

Компилятор С++ знает все, что ему требуется для трансляции этого кода в машинные инструкции - адрес переменной c, ее тип - CppClass, наличие метода SomeOperation и его точный адрес в памяти. Информация о типах используется компилятором и, как правило, не добавляется в выходной модуль.

Статическая типизация не запрещает компилятору встраивать информацию о типах:

// C++ с включенной поддержкой RTTI
DerivedCppClass derived;
BaseCppClass &base = derived;

...

DerivedCppClass &derived = dynamic_cast<DerivedCppClass&>(base);

Пользы от RTTI в С++ практически нет, поскольку единственное, что можно узнать о типе во время выполнения, - это его строковое имя, а наличие в коде операторов dynamic_cast свидетельствует о плохом понимании программистом принципов ООП.

В управляемых языках, таких как Java или C#, подробная информация о типе зашивается в выходной модуль и доступна для использования во время выполнения программы. Этот механизм называется интроспекцией (reflection) и имеет свою довольно высокую цену в виде многократной потери производительности.

Динамическая типизация (dynamic typing)

Python - хороший пример динамически типизированного языка:

class C(object):
   def hello():
      print 'Hello!'

def x(c):
   c.hello # WTF?!

c = C()
x(c)

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

Преимущества и недостатки статической (static) и динамической (dynamic) типизаций

Статическая (static) типизация

Преимущества:

  • Легче ориентироваться в коде
  • Хорошая поддержка со стороны средств разработки (рефакторинг, подсказки)
  • Скорость выполнения
  • Скорость выполнения
  • Скорость выполнения

Недостатки:

  • Усложнение языка (и, как последствие, увеличение стоимости разработки и поддержки)
  • Реализация полиморфизма через наследование
  • Ложное впечатление о корректности кода (компилируется - значит работает)
  • И еще

Динамическая (dynamic) типизация

Преимущества:

  • Хорошая поддержка современных стандартов и технологий (SOA, например)
  • Выразительность и простота кода
  • Скорость разработки
  • И еще

Недостатки:

  • Низкая производительность
  • Необходимость качественного и обширного тестирования

Дополнительные ссылки по теме:

Copyright 2007-2011 Chabster