Топ вещей из Java, которых мне не хватает в C#

8b92f6f2178a6eb24c4346457e6d7196.jpg

Спор «Java vs. C#» существует чуть меньше, чем вечность. Есть много статей, затрагивающих разные участки его спектра: Что есть в C# чего нет в Java, что языки друг у друга позаимствовали, у одних LINQ, у других обратная совместимость, в общем, тысячи их.

Однако, я никогда не видел, чтобы писали о чём-то, что в Java, с точки зрения фич языка есть, чего в C# нет. Впрочем, я здесь не для того, чтобы спорить. Эта статья призвана выразить моё субъективное мнение и заполнить небольшой пробел по теме, озвученной в заголовке.

Оба языка крутые, востребованные, у меня нет цели принизить один на фоне другого. Наоборот, хочу озвучить, что можно было бы, на мой взгляд, привнести, и порассуждать о том, насколько это нужно. Поэтому перейдём сразу к списку.

1. Class based Enum

Ни для кого не секрет, что в отличие от Java, в C# и C++ перечисления это именованные числовые константы. А что есть перечисления в Java? По сути, синтаксический сахар поверх класса. Напишем какое-нибудь перечисление, например. для хранения типов «слов», распознаваемых лексическим анализатором:

enum TokenType {
    IDENTIFIER,
    NUMBER,
    ASSIGN;
}

И поскольку перечисление это тот же класс, то можно накрутить конструктор, методы, поля, да даже реализацию интерфейса! Добавим возможность константам перечисления трансформироваться в регулярное выражение:

interface ToPattern {
    Pattern getPattern();
}

enum TokenType implements ToPattern {
    IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"),
    NUMBER("[0-9]+"),
    ASSIGN("[=]");
    
    private final String pattern;
    
    private TokenType(String pattern){
        this.pattern = pattern;
    }

    @Override
    public Pattern getPattern() {
        return Pattern.compile(pattern);
    }
}

А как сделать подобное в C#? Есть два варианта:

  1. Атрибуты и методы расширений с рефлексией (нельзя реализовывать интерфейсы):

[AttributeUsage(AttributeTargets.Field)]
internal class PatternAttribute : Attribute
{
    public string Pattern { get; }

    public PatternAttribute(string pattern) => 
        Pattern = pattern;
}

public enum TokenType
{
    [Pattern("[a-zA-Z][a-zA-Z0-9]*")] Identifier,
    [Pattern("[0-9]+")] Number,
    [Pattern("[=]")] Assign
}

public static class TokenTypeExtensions
{
    public static Regex GetRegex(this TokenType tokenType) =>
        new(typeof(TokenType)
            .GetField(tokenType.ToString())!
            .GetCustomAttribute()!
            .Pattern);
}
  1. Классы с публичными статическими константами:

interface IHasRegex
{
    Regex Regex { get; }
}

class TokenType : IHasRegex
{
    public static readonly TokenType Identifier =
        new("[a-zA-Z][a-zA-Z0-9]*");
    public static readonly TokenType Number =
        new("[0-9]+");
    public static readonly TokenType Assign =
        new("[=]");

    private readonly string _pattern;

    private TokenType(string pattern) =>
        _pattern = pattern;
    
    public Regex Regex => new(_pattern);
}

Напрашивается вопрос:

Зачем мне перечисления в C#, если я могу реализовывать их так, как они устроены в Java?

Особенно актуальный при наличии новых возможностей языка в последних версиях относительно ключевого слова switch.

2. Full support of covariant return types

Начиная с C# 9, в языке появилась возможность делать возвращаемые типы методов ковариантными. Если раньше код писался примерно так:

abstract record Fruit;

record Apple : Fruit;

record Orange : Fruit;

abstract class FruitFactory
    where TFruit : Fruit
{
    public abstract TFruit Create();
}

class AppleFactory : FruitFactory
{
    public override Apple Create() => new();
}

class OrangeFactory : FruitFactory
{
    public override Orange Create() => new();
}

То сейчас лишние конструкции можно опустить:

abstract class FruitFactory
{
    public abstract Fruit Create();
}

class AppleFactory : FruitFactory
{
    public override Apple Create() => new();
}

class OrangeFactory : FruitFactory
{
    public override Orange Create() => new();
}

В Java это было почти всегда, и сама возможность работала чуть шире. Она распространялась на реализацию и расширение интерфейсов. Например, я описываю структуру, которую можно копировать вместе данными. Для этого мне нужно указать, что данные копируются. За это отвечает контракт Cloneable. По умолчанию, метод clone возвращает Object. Однако, чтобы не засорять код кастами, я могу написать, что clone возвращает то, что копируется:

class Tree implements Cloneable {
    private final Node root;

    public Tree(Node root) {
        this.root = root;
    }

    @Override
    public Tree clone() throws CloneNotSupportedException {
        super.clone();
        return new Tree<>(root.clone());
    }
}

class Node implements Iterable>, Cloneable {
    private final T data;
    private final List> children;

    public Node(T data) {
        this.data = data;
        children = new ArrayList<>();
    }

    private void push(Node node) {
        children.add(node);
    }

    @Override
    public Iterator> iterator() {
        return new ArrayList<>(children).iterator();
    }

    @Override
    public Node clone() throws CloneNotSupportedException {
        super.clone();
        var node = new Node<>(data);
        for (var child : this) {
            node.push(child.clone());
        }
        return node;
    }
}

В C# так сделать нельзя, выйдет ошибка:

Method 'Clone' cannot implement method from interface 'System.ICloneable'. Return type should be 'object'.

class Foo : ICloneable
{
    public Foo Clone()
    {
        throw new NotImplementedException();
    }
}

Почему у интерфейсов ещё нет ковариантности возвращаемого типа — вопрос открытый, даже в спецификации языка.

3. Functional Interfaces

В Java есть понятие функциональный интерфейс. Функциональный интерфейс (functional interface) — интерфейс с единственным абстрактным методом. Основная фишка таких интерфейсов в том, что их экземпляры можно инициализировать с помощью лямбда выражений (начиная с Java 8):

@FunctionalInterface
interface IntegerBinaryExpression {
    int evaluate(int a, int b);
}

// ...

IntegerBinaryExpression add = (a, b) -> a + b;
System.out.println(add.evaluate(3, 5)); // 8

Однако, о том, почему именно так всё устроено, нетрудно догадаться, если посмотреть, на что предлагает заменить IDE значение, присваиваемое переменной add типа IntegerBinaryExpression:

IntelliJ IDEAIntelliJ IDEA

Если нажать на предлагаемый replace, то получим:

IntegerBinaryExpression add = Integer::sum;

Всё это, вместе с синтаксисом «пуговицы» (::), говорит об одном: функциональные интерфейсы — всего лишь механизм реализации callback’ов в Java. В C# есть делегаты, поэтому надобность в подобном сахаре крайне сомнительна, хоть и выглядит удобно, особенно для интерфейсов, экземпляры которых используются в проекте единожды.

4. Anonymous interface implementation

Предыдущий пример, возможно, стоило рассмотреть именно в этой секции, поскольку он является демонстрацией частного случая крутой, на мой взгляд, фичи Java — анонимная реализация интерфейсов.

Возьмём теперь контракт, у которого больше двух методов:

interface Pair {
    F first();

    S second();
}

И если начать набирать new Pair для создания экземпляра интерфейса, то нам не выскочит ошибка о том, что нельзя создавать инстансы абстрактных сущностей, а предложение реализовать методы:

var myPair = new Pair() {
    @Override
    public String first() {
        return "first";
    }

    @Override
    public Integer second() {
        return 2;
    }
};

Также такие штуки можно проворачивать и с классами (абстрактными и не очень):

class Book {
    public void read() {
        // ...
    }
}

// ...

var myBook = new Book() {
    @Override
    public void read() {
        super.read();
    }
};

Эта фича открывает новые возможности для создания программного обеспечения в случаях, когда надо не раздувать структуру проекта и на лету создавать новые реализации контрактов, или необходимо инкапсулировать какие-то специфичные сценарии использования контракта. Безусловно, жду в C#, все возможности у CLR для этого есть. В репозитории Roslyn даже есть feature request.

Заключение

Поделился с Вами о своих взглядах о возможных направлениях развития языка программирования C# и освятил, ранее не тронутую тему, о том, чего в C# нет, что в Java есть. Надеюсь, было интересно и полезно! Спасибо, что прочитали!

Ещё я веду telegram канал StepOne, где оставляю много интересных заметок про коммерческую разработку и мир IT глазами эксперта.

© Habrahabr.ru