Топ вещей из Java, которых мне не хватает в C#
Спор «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()!
.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
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 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 глазами эксперта.