Крадущийся тигр, затаившийся дракон

в 10:17, , рубрики: .net, clr, generics, java, jvm, linq, Программирование, метки: , , , , , ,

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js