Участник:Phersu/Java vs. CSharp

Материал из LingvoWiki
Перейти к навигацииПерейти к поиску

Адаптеры (анонимные классы) vs. делегаты

Адаптеры в Java более flexible, так как могут хранить своё состояние между вызовами (что бывает очень полезно). В случае с делегатами в C# состояние предполагается хранить во внешнем scope, что замусоривает код. И делегаты, и адаптеры поддерживают замыкания и отличаются только синтаксисом.

Синтаксис у делегатов покороче, однако адаптеры позволяют объединять несколько методов в один логический listener, благодаря чему в итоге можно сделать API в Java компактнее:

C#:

public delegate void ClickDelegate(object sender, ClickEventArgs e);
public delegate void MoveDelegate(object sender, MoveEventArgs e);
public delegate void KeyDelegate(object sender, KeyEventArgs e);
void obj_onClick(object sender, ClickEventArgs e) { }
void obj_onMove(object sender, MoveEventArgs e) { }
void obj_onKey(object sender, KeyEventArgs e) { }
obj.Click += obj_onClick;
obj.Move += obj_onMove;
obj.Key += obj_onKey;

Java:

public abstract class Listener {
public void onClick(ClickEvent e) { }
public void onMove(MoveEvent e) { }
public void onKey(KeyEvent e) { }
}
obj.addListener(new Listener() {
public void onClick(ClickEvent e) { }
public void onMove(MoveEvent e) { }
public void onKey(KeyEvent e) { }
});

Также делегаты суть second-class citizens, чужды ООП, усложняют язык и VM, вводят дополнительные ненужные типы (MulticastDelegate наследует Delegate, при том что реально используется только класс MulticastDelegate, который нельзя наследовать (хотя он не sealed!), а класс Delegate вообще никак не используется и является технической ошибкой проектирования в .NET 1.0)

См. http://java.sun.com/docs/white/delegates.html

Ещё немного о замыканиях

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

Напр.:

final int i = 10; // НЕ КОМПИЛИРУЕТ, ЕСЛИ НЕ FINAL
Object obj = new Object() {
public int hashCode() {
return i;
}
};
i++; // ОШИБКА КОМПИЛЯЦИИ
System.out.println(obj.hashCode());

Что же C#? А C# пофиг. Он без проблем скомпилирует данный кусок (без модификатора final) и на консоль тихо выведет 10 вместо предполагаемого 11.

Generics

Параметризированные типы в C# поддерживаются на уровне VM, в Java же имеем type erasure. С одной стороны, generics в CLR более производительны: нет cast'ов и boxing/unboxing, как в Java. С другой стороны, на уровне языка C# и системы CLR вводятся очередные хаки:

  • примитивные типы наследуют ValueType, который наследует Object, но в то же время примитивные типы по семантике и поведению совсем не являются Object'ами и, наоборот, противопоставляются им (ну что общего у int и Object?)
  • вызов виртуальных методов на value types нуждается в магическом опкоде constrained
  • каждый параметризированный класс (напр. List<int>) имеет в памяти специальный инстанцированный объект System.Type, каждый со своими сгенерированными JIT-данными (хотя сейчас уже даже mono умеет sharing), каждый со своими внутренними структурами
  • противоречит ООП-иерархии, дублирует на уровне системы типов саму первоначальную идею, которая заключается в том, что все классы наследуют Object именно с целью иметь обобщённые контейнеры с апкастом в Object

Среди плюсов, конечно — лучшая производительность, возможность определить тип T для конкретно взятого контейнера. В Java же подход другой: generics не стоят в центре системы типов, а являются просто удобным инструментом для проверки типов в compile-time.

Минусы Java

  • примитивные объекты должны оборачиваться в wrappers типа Integer, Float и т.д. на куче. Зато это позволяет иметь систему типов довольно семантически интуитивной и без лишних хаков. А вот в C# даже boxed primitive types умудрились сделать через хак: они являются очередным «магическим» типом, реализуемом где-то в недрах VM, в то время как в Java это просто объекты с одним единственным полем int, float и т.д.
  • при добавлении объекта в контейнер происходит апкаст, который в 100% случаях no op, при извлечении же объекта нужен даункаст, но я не думаю, что это серьёзный bottleneck (бенчмарки?) Думаю, что JIT имеет возможность соптимизировать здесь.
  • нет возможности определить параметр типа в рантайме, напр. в Java нельзя написать new T[10] — нужно писать new Object[10].
  • overhead из-за boxing'а довольно большой, напр. заголовок объекта на куче равен как минимум 8 байтам + выравнивание на 8 байт + ссылка на объект из внутр. массива — итого один int в контейнере занимает 20 байт vs. 4 байта в C#.

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

JIT

В CLR абсолютно все методы при первом вызове компилируются в машинный код пользовательского процессора. Из-за такого дизайна — из-за того что огромное количество методов должно быть на лету скомпилировано без видимых пауз — кодогенератор C#/CLR обречён выдавать некачественный малопроизводительный код, так как в JIT-системах качество/производительность кода обратно пропорциональны количеству компилируемого кода.

В JVM же код по умолчанию интерпретируется, а компилируются только самые вызываемые методы, что позволяет сэкономить время на качественную компиляцию нескольких актуальных методов вместо траты времени на некачественную компиляцию ВСЕХ методов. Более того, JVM умеет динамически адаптироваться и деоптимизировать/переоптимизировать уже скомпилированные методы, если ситуация с прошлого раза кардинально изменилась. Дизайн C#/CLR не позволяет ничего из этого делать.

Для обхода такой трагедии .NET вроде как предоставляет утилиту NGEN для прегенерации машинного кода, однако сам же MSDN и говорит, что получаемый код ещё медленнее, чем в JIT'е (то есть тот всё-таки делает кое-какие ограниченные динамические оптимизации).

Ещё о value types

В C# есть user-defined value types, живущие в стеке. Они полезны в двух случаях: при interop и при небольших утилитных иммутабельных объектах типа Vector или Matrix. В Java user-defined value types нет, т.к. interop через JNI предполагает контролированный ручной маршалинг, а что касается иммутабельных объектов, то эта проблема уже неактуальна в связи с введением escape analysis в Java 7. Единственно, конечно — если объект возвращается из функции, то он будет создан в куче в Java vs. на стеке в C#, но зато хотя бы промежуточные объекты в Java выделяется в стеке.

Таким образом, в очередной раз, Java выбирает более чистый дизайн и переносит решение проблем производительности с дизайна системы на реализацию системы. Создаётся объект на куче или в стеке — это должна решать VM, а не программист, т.к. это вопрос реализации, а не дизайна. Не очень хорошо, что в 21 веке язык C#, позиционируемый как современный, всё ещё перекладывает на программиста задачи, которые обычно встречаются в языках типа C.

Собственные грабли

P.S. Смешно ли — команда C# впоследствии стала бороться сама с собой, введя класс Nullable<T> where T: struct для структур, которые могут быть нулём (при том что по спецификации структуры не могут быть нулём). В Java всё это время можно было использовать те самые java.lang.Integer, java.lang.Float и т.д., от которых C#-исты отказались.

Enum

В C#/CLR перечисляемые типы, enum'ы, могут накладываться только на integer-типы:

enum : int
{
}

Это сильно негибко, и такой выбор был обусловлен чисто interop-соображениями: в C/C++ enum'ы тоже являются простыми целочисленными значениями и, таким образом, предполагалось, что проще чем 1-к-1 маппинг в managed-to-native transitions не найти.

Но в Java выждали время и в очередной раз решили задачу куда мудрее. Да, первоначальный стандарт Java вообще не имел enum'ов и зависел от нисколь не типизированных целочисленных констант. Но парни исправились и ввели такую штуку: каждое enum-значение это просто навсего синглтон, автоматически генерируемый компилятором. Так как каждое enum-значение является обычным объектом, его можно связывать с любым значением, через поля объекта. Если нам нужен interop, то мы можем связать enum с численным значением:

public enum MeshUsage {
STATIC(GL_STATIC_DRAW),
DYNAMIC(GL_DYNAMIC_DRAW),
STREAM(GL_STREAM_DRAW);
public final int id;
MeshUsage(int id) {
this.id = id;
}
}


и использовать таким образом:

MeshUsage usage = getUsage();
NativeFunctions.doSomething(usage.id);


Но енто ещё не всё: каждый enum располагает методом name(), а также можно итерировать по списку:

for(ImageFormat imf: ImageFormat.values()) {
if(extension.equals(imf.name())) {
format = imf;
break;
}
}

Лепота.

TODO

final