Java vs. C#… Что может быть лучше вечного спора? Нет, данная статья не посвящена очередному бенчмарку, и даже не является holy war, не стоит даже вопрос: «кто круче».
Для каждой задачи существует свой инструмент. Сравнивать C# и Ruby, например, не имеет смысла, т.к. их целевое предназначение совершенно разное, да и природа также. Однако именно C# и Java являются наиболее близкими по своей философии.
Очень часто коллеги, пишущие на Java, даже не подозревают о многих (!!!) вещах, которые предоставляет (или, наоборот не предоставляет) C#.
Если Вам интересно посмотреть на C# и Java без субъективизма, а также узнать внутреннее устройство той или иной возможности, тогда вперед.
Язык C# появился в 2001 году, а его разработка была начата еще в 1999 гг. Тогда он был весьма схож с Java 1.4. Однако современный C#, которого мы знаем, следует начинать рассматривать с версии 2.0 (что соответствует времени выхода Java 5).
Бытует мнение, что C# многое заимствует из Java. Однако я категорически не согласен с этим. По-моему мнению, C# во многом является C «с объектами», или же C++ «с человеческим лицом».
Надеюсь, что статья и доводы в ней подтвердят данное утверждение.
Пересказывать википедию я не буду. Поэтому сразу же двинемся дальше и пройдемся по ключевым отличиям и преимуществам.
Сначала мы посмотрим на возможности самих JVM и CLR (Common Language Runtime), далее уже рассмотрим синтаксический сахар C#.
И .NET, и Java используют bytecode. Конечно, кроме самого формата, существует одно очень важное различие – полиморфность.
CIL (Common Intermediate Language, он же MSIL, он же просто IL) – является байт-кодом с полиморфными (обобщенными) инструкциями.
Так, если в Java используется отдельная инструкция для каждого типа операций с различными типами (например: fadd – сложение 2-х float, iadd – сложение 2-х integer), то в CIL для каждого вида операций существует лишь одна инструкция с полиморфными параметрами (например, существует только одна инструкция – add, производящая сложение и float, и integer). Вопрос решения генерации соответствующих x86-инструкций ложится на JIT.
Количество инструкций у обеих платформ примерно одинаковое. Сравнивая список команд байт-кода Java и CIL, видно, что 206 у Java, и 232 — CIL, однако не забываем, что у Java многие команды просто повторяют функционал друг друга.
Как известно, в Java используется механизм type erasure, т.е. поддержка generics обеспечивается лишь компилятором, но не рантаймом, и после компиляции информация о самом типе не будет доступна.
Например, код:
Выведет true.
При этом вместо обобщенного типа T создается экземпляр объекта типа java.lang.Object.
Будет преобразован к виду:
Таким образом, вся политика безопасности типов разрушается моментально.
.NET, наоборот, имеет полную поддержку generics как compile-time, так и run-time.
Так что же необходимо реализовать для полной поддержки generics в Java? Вместо этого лучше посмотрим, что сделала команда разработчиков .NET’a:
Т.е. generics доступны не только во время компиляции, но и во время исполнения без потери или изменения информации о типе. Также исчезает потребность в boxing/unboxing.
Java является полностью ОО-языком. С этим можно поспорить. И вот почему: примитивные типы (integer, float и т.п.) не наследуются от java.lang.Object. Поэтому generics в Java не поддерживают primitive types.
А ведь в по-настоящему ОО-языке everything is object.
Это не единственное ограничение. Также невозможно создать собственные примитивные типы.
C# позволяет это делать. Имя этим структурам – struct.
Например:
Также generics в C# позволяют использовать value types (int, float и т.д.)
Если в Java необходимо писать так:
Но нельзя так:
C# позволяет использование примитивных типов.
Теперь мы подошли к теме иерархии типов в .NET.
В .NET существует 3 вида типов: value, reference и pointer types.
Итак, value type – аналог primitive type из Java. Однако наследуется от System.Object, живет не в куче, а в стеке (а теперь оговорка: расположение value type зависит от его жизненного цикла, например, при участии в замыкании автоматически происходит boxing).
Reference type – представляет собой то же самое, что и reference types в Java.
Pointer type – является самым необычным свойством .NET’a. Дело в том, что CLR позволяет работать с указателями напрямую!
Например:
Очень похоже на C++ код, не так ли?
Сначала определимся, что же умеет C#:
Свойства в C# представляют синтаксический сахар, т.к. при компиляции превращаются в методы типа GetXXX, SetXXX. Однако информация о самом понятии свойство сохраняется в метаданных, поэтому из любого другого поддерживающего свойства языка мы можем обратиться к нему только как object.PropertyX, а не object.GetPropertyX.
Например:
Будет преобразовано к виду:
Делегаты являются аналогами указателей на методы в C/C++. Однако являются типобезопасными. Их главное предназначение – callback функции, а также работа с событиями.
При этом делегаты в .NET – полноценные объекты.
Начиная с появления DLR (Dynamic Language Runtime – основа для динамических языков на .NET), а также dynamic в C# 4 – играет ключевую роль в поддержке динамизма. Для более детального ознакомления советую почитать мою статью Погружаемся в глубины C# dynamic.
Данный подход коренным образом отличается от проекта Da Vinci для Java, т.к. в последнем пытаются расширить саму VM.
Рассмотрим пример на C#:
А также на C:
Итак, что же мы видим? Если на C мы можем передать указатель на функцию с другими типами параметров (скажем float arg1, float arg2), то в C# — это невозможно. В C# делегаты проходят не только проверку сигнатуры и типов на этапе компиляции, но и в рантайме.
События необходимы для реализации событийно-ориентированного программирования. Конечно, можно обойтись и EventDispatcher’ом, или паттерном Publisher/Subscriber. Однако нативная поддержка со стороны языка дает весомые преимущества. Одним из которых является типобезопасность.
Например:
Анонимные методы существенно упрощают жизнь – структура класса остается чистой, т.е. не нужно создавать отдельные лишние методы в самом классе.
Изменим вышеприведенный пример с бинарными операциями с использованием анонимных методов:
Не правда ли более коротко и чисто?
Рассмотрим теперь лямбда-выражения.
Лямбда-выражение — это анонимная функция, которая содержит выражения и операторы, а также может использоваться для создания делегатов или дерева выражений.
А как же будет выглядеть пример с событиями? Очень просто:
Что ж, мы сократили код еще больше и уже это становится похожим на функциональный стиль (!!!). Да, C# является также и функциональным языком, т.к. функции являются объектами первого класса.
Лямбда-выражения, а вместе с ними и деревья выражений были созданы вместе с LINQ (Language Integrated Query).
Все еще не знакомы с LINQ? Хотите увидеть как будет выглядеть знаменитый MapReduce на LINQ?
Или же использовать SQL-подобный синтаксис:
В этом примере мы видим и LINQ (GroupBy().Select().Where() и т.д.), и анонимные классы –
Хм…что же еще здесь используется? Ответ прост – мощная система вывода типов.
Главную роль здесь играет ключевое слово var. C++ 11 имеет аналогичную конструкцию auto.
Так без вывода типов нам пришлось бы писать так:
[Данный метод сгенерировал за нас компилятор]
Для описания всех остальных возможностей C#, настроек его компилятора и т.п. необходимо посвятить еще не одну статью.
А между тем, C# 5 – который уже доступен и скоро будет его официальный релиз, добавляет асинхронное программирование, появившееся также и в C++ 11!
C# и Java являются мощными языками, а также мощными платформами (.NET и Java). Как я уже писал в начале статьи — для каждой задачи существует свой инструмент.
C# — не является продолжением или копирующим Java. Даже когда он разрабатывался в Microsoft, его кодовое название было COOL (C-like Object Oriented Language). Сколько раз в данной статье приводилась аналогия с C/C++? Достаточное количество.
Надеюсь, что моя статья помогла решить (хотя бы немного) вопрос разности и языков, и платформ.
Спасибо за внимание!
Для каждой задачи существует свой инструмент. Сравнивать C# и Ruby, например, не имеет смысла, т.к. их целевое предназначение совершенно разное, да и природа также. Однако именно C# и Java являются наиболее близкими по своей философии.
Очень часто коллеги, пишущие на Java, даже не подозревают о многих (!!!) вещах, которые предоставляет (или, наоборот не предоставляет) C#.
Если Вам интересно посмотреть на C# и Java без субъективизма, а также узнать внутреннее устройство той или иной возможности, тогда вперед.
▌Немного истории
Язык C# появился в 2001 году, а его разработка была начата еще в 1999 гг. Тогда он был весьма схож с Java 1.4. Однако современный C#, которого мы знаем, следует начинать рассматривать с версии 2.0 (что соответствует времени выхода Java 5).
Бытует мнение, что C# многое заимствует из Java. Однако я категорически не согласен с этим. По-моему мнению, C# во многом является C «с объектами», или же C++ «с человеческим лицом».
Надеюсь, что статья и доводы в ней подтвердят данное утверждение.
Пересказывать википедию я не буду. Поэтому сразу же двинемся дальше и пройдемся по ключевым отличиям и преимуществам.
Сначала мы посмотрим на возможности самих JVM и CLR (Common Language Runtime), далее уже рассмотрим синтаксический сахар C#.
▌Эпизод I: Bytecode
И .NET, и Java используют bytecode. Конечно, кроме самого формата, существует одно очень важное различие – полиморфность.
CIL (Common Intermediate Language, он же MSIL, он же просто IL) – является байт-кодом с полиморфными (обобщенными) инструкциями.
Так, если в Java используется отдельная инструкция для каждого типа операций с различными типами (например: fadd – сложение 2-х float, iadd – сложение 2-х integer), то в CIL для каждого вида операций существует лишь одна инструкция с полиморфными параметрами (например, существует только одна инструкция – add, производящая сложение и float, и integer). Вопрос решения генерации соответствующих x86-инструкций ложится на JIT.
Количество инструкций у обеих платформ примерно одинаковое. Сравнивая список команд байт-кода Java и CIL, видно, что 206 у Java, и 232 — CIL, однако не забываем, что у Java многие команды просто повторяют функционал друг друга.
▌Эпизод III: Generics (parameterized types || parametric polymorphism)
Как известно, в Java используется механизм type erasure, т.е. поддержка generics обеспечивается лишь компилятором, но не рантаймом, и после компиляции информация о самом типе не будет доступна.
Например, код:
List<String> strList = new ArrayList<String>(); List<Integer> intList = new ArrayList<Integer>(); bool areSame = strList.getClass() == intList.getClass(); System.out.println(areSame);
Выведет true.
При этом вместо обобщенного типа T создается экземпляр объекта типа java.lang.Object.
List<String> strList = new ArrayList<String>(); strList.add("stringValue"); String str = strList.get(0);
Будет преобразован к виду:
List list = new ArrayList(); list.add("stringValue"); String x = (String) list.get(0);
Таким образом, вся политика безопасности типов разрушается моментально.
.NET, наоборот, имеет полную поддержку generics как compile-time, так и run-time.
Так что же необходимо реализовать для полной поддержки generics в Java? Вместо этого лучше посмотрим, что сделала команда разработчиков .NET’a:
- Поддержка generics на уровне Common Type System для кросс-языкового взаимодействия
- Полная поддержка со стороны Reflection API
- Добавление новых классов/методов в базовые классы (Base Class Library)
- Изменения в самом формате и таблицах метаданных
- Изменения в алгоритме выделения памяти рантаймом
- Добавления новых структур данных
- Поддержка generics со стороны JIT (code-sharing)
- Изменения в формате CIL (новые инструкции байт-кода)
- Поддержка со стороны верификатора CIL-кода
Т.е. generics доступны не только во время компиляции, но и во время исполнения без потери или изменения информации о типе. Также исчезает потребность в boxing/unboxing.
▌Эпизод IV: Types
А ведь в по-настоящему ОО-языке everything is object.
Это не единственное ограничение. Также невозможно создать собственные примитивные типы.
C# позволяет это делать. Имя этим структурам – struct.
Например:
public struct Quad { int X1; int X2; int Y1; int Y2; }
Также generics в C# позволяют использовать value types (int, float и т.д.)
Если в Java необходимо писать так:
List<Integer> intList = new ArrayList<Integer>();
Но нельзя так:
List<Integer> intList = new ArrayList<int>();
C# позволяет использование примитивных типов.
Теперь мы подошли к теме иерархии типов в .NET.
В .NET существует 3 вида типов: value, reference и pointer types.
Итак, value type – аналог primitive type из Java. Однако наследуется от System.Object, живет не в куче, а в стеке (а теперь оговорка: расположение value type зависит от его жизненного цикла, например, при участии в замыкании автоматически происходит boxing).
Reference type – представляет собой то же самое, что и reference types в Java.
Pointer type – является самым необычным свойством .NET’a. Дело в том, что CLR позволяет работать с указателями напрямую!
Например:
struct Point { public int x; public int y; } unsafe static void PointerMethod() { Point point; Point* p = &point; p->x = 100; p->y = 200; Point point2; Point* p2 = &point2; (*p2).x = 100; (*p2).y = 200; }
Очень похоже на C++ код, не так ли?
▌Эпизод V: Возможности C#
Сначала определимся, что же умеет C#:
- Свойства (в том числе автоматические)
- Делегаты
- События
- Анонимные методы
- Лямбда-выражения
- LINQ
- Expression Trees
- Анонимные классы
- Мощный вывод типов
- Перегрузка операторов
- Indexers
- …еще много чего
Свойства в C# представляют синтаксический сахар, т.к. при компиляции превращаются в методы типа GetXXX, SetXXX. Однако информация о самом понятии свойство сохраняется в метаданных, поэтому из любого другого поддерживающего свойства языка мы можем обратиться к нему только как object.PropertyX, а не object.GetPropertyX.
Например:
public class TestClass { public int TotalSum { get { return Count * Price; } } //автоматическое свойство - компилятор сам сгенерирует поля public int Count { get; set; } public int Price { get { return 50; } } }
Будет преобразовано к виду:
public class TestClass { /* *весь остальной код */ private int <Count>k__BackingField; //автоматическое свойство - компилятор сам сгенерирует поля public int Count { get { return <Count>k__BackingField; } set { <Count>k__BackingField = value; } } }
Делегаты являются аналогами указателей на методы в C/C++. Однако являются типобезопасными. Их главное предназначение – callback функции, а также работа с событиями.
При этом делегаты в .NET – полноценные объекты.
Начиная с появления DLR (Dynamic Language Runtime – основа для динамических языков на .NET), а также dynamic в C# 4 – играет ключевую роль в поддержке динамизма. Для более детального ознакомления советую почитать мою статью Погружаемся в глубины C# dynamic.
Данный подход коренным образом отличается от проекта Da Vinci для Java, т.к. в последнем пытаются расширить саму VM.
Рассмотрим пример на C#:
public class TestClass { public delegate int BinaryOp(int arg1, int arg2); public int Add(int a, int b) { return a + b; } public int Multiply(int first, int second) { return first * second; } public void TestDelegates() { BinaryOp op = new BinaryOp(Add); int result = op(1, 2); Console.WriteLine(result); //выведет: 3 op = new BinaryOp(Multiply); result = op(2, 5); Console.WriteLine(result); //выведет: 10 } }
А также на C:
int Add(int arg1, int arg2) { return arg1 + arg2; } void TestFP() { int (*fpAdd)(int, int); fpAdd = &Add; //указатель на функцию int three = fpAdd(1, 2); // вызываем функцию через указатель }
Итак, что же мы видим? Если на C мы можем передать указатель на функцию с другими типами параметров (скажем float arg1, float arg2), то в C# — это невозможно. В C# делегаты проходят не только проверку сигнатуры и типов на этапе компиляции, но и в рантайме.
События необходимы для реализации событийно-ориентированного программирования. Конечно, можно обойтись и EventDispatcher’ом, или паттерном Publisher/Subscriber. Однако нативная поддержка со стороны языка дает весомые преимущества. Одним из которых является типобезопасность.
Например:
public class MyClass { private string _value; public delegate void ChangingEventhandler(string oldValue); public event ChangingEventhandler Changing; public void OnChanging(string oldvalue) { ChangingEventhandler handler = Changing; if (handler != null) handler(oldvalue); } public string Value { get { return _value; } set { OnChanging(_value); _value = value; } } public void TestEvent() { MyClass instance = new MyClass(); instance.Changing += new ChangingEventhandler(instance_Changing); instance.Value = "new string value"; //будет вызван метод instance_Changing } void instance_Changing(string oldValue) { Console.WriteLine(oldValue); } }
Анонимные методы существенно упрощают жизнь – структура класса остается чистой, т.е. не нужно создавать отдельные лишние методы в самом классе.
Изменим вышеприведенный пример с бинарными операциями с использованием анонимных методов:
public class TestClass { public delegate int BinaryOp(int arg1, int arg2); public void TestDelegates() { BinaryOp op = new BinaryOp(delegate(int a, int b) { return a + b; }); int result = op(1, 2); Console.WriteLine(result); //выведет: 3 } }
Не правда ли более коротко и чисто?
Рассмотрим теперь лямбда-выражения.
Лямбда-выражение — это анонимная функция, которая содержит выражения и операторы, а также может использоваться для создания делегатов или дерева выражений.
public class TestClass { public delegate int BinaryOp(int arg1, int arg2); public void TestDelegates() { BinaryOp op = new BinaryOp((a, b) => a + b); int result = op(1, 2); Console.WriteLine(result); //выведет: 3 } }
А как же будет выглядеть пример с событиями? Очень просто:
public class MyClass { /* * весь остальной код */ public void TestEvent() { MyClass instance = new MyClass(); instance.Changing += (o) => Console.WriteLine(o); instance.Value = "new string value"; //будет вызван Console.WriteLine } }
Что ж, мы сократили код еще больше и уже это становится похожим на функциональный стиль (!!!). Да, C# является также и функциональным языком, т.к. функции являются объектами первого класса.
Лямбда-выражения, а вместе с ними и деревья выражений были созданы вместе с LINQ (Language Integrated Query).
Все еще не знакомы с LINQ? Хотите увидеть как будет выглядеть знаменитый MapReduce на LINQ?
public static class MyClass { public void MapReduceTest() { var words = new[] {"...some text goes here..."}; var wordOccurrences = words .GroupBy(w => w) .Select(intermediate => new { Word = intermediate.Key, Frequency = intermediate.Sum(w => 1) }) .Where(w => w.Frequency > 10) .OrderBy(w => w.Frequency); } }
Или же использовать SQL-подобный синтаксис:
public void MapReduceTest() { string[] words = new string[] { "...some text goes here..." }; var wordOccurrences = from w in words group w by w into intermediate select new { Word = intermediate.Key, Frequency = intermediate.Sum((string w) => 1) } into w where w.Frequency > 10 orderby w.Frequency select w; }
В этом примере мы видим и LINQ (GroupBy().Select().Where() и т.д.), и анонимные классы –
new { Word = intermediate.Key, Frequency = intermediate.Sum(w => 1) }
Хм…что же еще здесь используется? Ответ прост – мощная система вывода типов.
Главную роль здесь играет ключевое слово var. C++ 11 имеет аналогичную конструкцию auto.
Так без вывода типов нам пришлось бы писать так:
public void MapReduceTest() { string[] words = new string[] { "...some text goes here..." }; var wordOccurrences = Enumerable.OrderBy(Enumerable.Where(Enumerable.Select(Enumerable.GroupBy<string, string>(words, delegate (string w) { return w; }), delegate (IGrouping<string, string> intermediate) { return new { Word = intermediate.Key, Frequency = Enumerable.Sum<string>(intermediate, (Func<string, int>) (w => 1)) }; }), delegate (<>f__AnonymousType0<string, int> w) { return w.Frequency > 10; }), delegate (<>f__AnonymousType0<string, int> w) { return w.Frequency; }); }
[Данный метод сгенерировал за нас компилятор]
Для описания всех остальных возможностей C#, настроек его компилятора и т.п. необходимо посвятить еще не одну статью.
А между тем, C# 5 – который уже доступен и скоро будет его официальный релиз, добавляет асинхронное программирование, появившееся также и в C++ 11!
▌Заключение
C# и Java являются мощными языками, а также мощными платформами (.NET и Java). Как я уже писал в начале статьи — для каждой задачи существует свой инструмент.
C# — не является продолжением или копирующим Java. Даже когда он разрабатывался в Microsoft, его кодовое название было COOL (C-like Object Oriented Language). Сколько раз в данной статье приводилась аналогия с C/C++? Достаточное количество.
Надеюсь, что моя статья помогла решить (хотя бы немного) вопрос разности и языков, и платформ.
Спасибо за внимание!
