Топ вещей из Java, которых мне не хватает в C#
Автор: StefanioСпор "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#? Есть два варианта:
- Атрибуты и методы расширений с рефлексией (нельзя реализовывать интерфейсы):
[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<PatternAttribute>()! .Pattern); }
- Классы с публичными статическими константами:
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<TFruit> where TFruit : Fruit { public abstract TFruit Create(); } class AppleFactory : FruitFactory<Apple> { public override Apple Create() => new(); } class OrangeFactory : FruitFactory<Orange> { 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<T> implements Cloneable { private final Node<T> root; public Tree(Node<T> root) { this.root = root; } @Override public Tree<T> clone() throws CloneNotSupportedException { super.clone(); return new Tree<>(root.clone()); } } class Node<T> implements Iterable<Node<T>>, Cloneable { private final T data; private final List<Node<T>> children; public Node(T data) { this.data = data; children = new ArrayList<>(); } private void push(Node<T> node) { children.add(node); } @Override public Iterator<Node<T>> iterator() { return new ArrayList<>(children).iterator(); } @Override public Node<T> 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
:
Если нажать на предлагаемый replace, то получим:
IntegerBinaryExpression add = Integer::sum;
Всё это, вместе с синтаксисом "пуговицы" (::
), говорит об одном: функциональные интерфейсы - всего лишь механизм реализации callback'ов в Java. В C# есть делегаты, поэтому надобность в подобном сахаре крайне сомнительна, хоть и выглядит удобно, особенно для интерфейсов, экземпляры которых используются в проекте единожды.
4. Anonymous interface implementation
Предыдущий пример, возможно, стоило рассмотреть именно в этой секции, поскольку он является демонстрацией частного случая крутой, на мой взгляд, фичи Java - анонимная реализация интерфейсов.
Возьмём теперь контракт, у которого не меньше двух методов:
interface Pair<F, S> { F first(); S second(); }
И если начать набирать new Pair
для создания экземпляра интерфейса, то нам не выскочит ошибка о том, что нельзя создавать инстансы абстрактных сущностей, а предложение реализовать методы:
var myPair = new Pair<String, Integer>() { @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 есть. Надеюсь, было интересно и полезно! Спасибо, что прочитали!