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#.
▌Эпизод 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("Hi");
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
Java является полностью ОО-языком. С этим можно поспорить. И вот почему: примитивные типы (integer, float и т.п.) не наследуются от java.lang.Object. Поэтому generics в Java не поддерживают primitive 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 string <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# и Java являются мощными языками, а также мощными платформами (.NET и Java). Как я уже писал в начале статьи — для каждой задачи существует свой инструмент.
C# — не является продолжением или копирующим Java. Даже когда он разрабатывался в Microsoft, его кодовое название было COOL (C-like Object Oriented Language). Сколько раз в данной статье приводилась аналогия с C/C++? Достаточное количество.
Надеюсь, что моя статья помогла решить (хотя бы немного) вопрос разности и языков, и платформ.
Спасибо за внимание!
Автор: szKarlen