Участник:Phersu/Java vs. CSharp: различия между версиями
Phersu (обсуждение | вклад) (Новая страница: «== Адаптеры (анонимные классы) vs. делегаты == Адаптеры в Java более flexible, так как могут хранить …») |
Phersu (обсуждение | вклад) |
||
(не показано 12 промежуточных версий этого же участника) | |||
Строка 6: | Строка 6: | ||
C#: | C#: | ||
− | :: public delegate void ClickDelegate(); | + | :: public delegate void ClickDelegate(object sender, ClickEventArgs e); |
− | :: public delegate void MoveDelegate(); | + | :: public delegate void MoveDelegate(object sender, MoveEventArgs e); |
− | :: public delegate void KeyDelegate(); | + | :: public delegate void KeyDelegate(object sender, KeyEventArgs e); |
− | :: obj. | + | :: void obj_onClick(object sender, ClickEventArgs e) { } |
− | :: obj. | + | :: void obj_onMove(object sender, MoveEventArgs e) { } |
− | :: obj. | + | :: void obj_onKey(object sender, KeyEventArgs e) { } |
+ | :: obj.Click += obj_onClick; | ||
+ | :: obj.Move += obj_onMove; | ||
+ | :: obj.Key += obj_onKey; | ||
Java: | Java: | ||
+ | :: public abstract class Listener { | ||
+ | :::: public void onClick(ClickEvent e) { } | ||
+ | :::: public void onMove(MoveEvent e) { } | ||
+ | :::: public void onKey(KeyEvent e) { } | ||
+ | :: } | ||
:: obj.addListener(new Listener() { | :: obj.addListener(new Listener() { | ||
− | ::: public void onClick() { } | + | ::: public void onClick(ClickEvent e) { } |
− | ::: public void onMove() { } | + | ::: public void onMove(MoveEvent e) { } |
− | ::: public void onKey() { } | + | ::: 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 | См. 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 |
Текущая версия на 20:16, 22 февраля 2012
Адаптеры (анонимные классы) 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) { }
- });
- public abstract class Listener {
Также делегаты суть 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;
- }
- public int hashCode() {
- };
- 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 enum MeshUsage {
- public final int id;
- MeshUsage(int id) {
- this.id = id;
- }
- MeshUsage(int id) {
- }
и использовать таким образом:
- MeshUsage usage = getUsage();
- NativeFunctions.doSomething(usage.id);
Но енто ещё не всё: каждый enum располагает методом name(), а также можно итерировать по списку:
- for(ImageFormat imf: ImageFormat.values()) {
- if(extension.equals(imf.name())) {
- format = imf;
- break;
- }
- if(extension.equals(imf.name())) {
- }
- for(ImageFormat imf: ImageFormat.values()) {
Лепота.
TODO
final