
Спор "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 есть. Надеюсь, было интересно и полезно! Спасибо, что прочитали!
Ещё я веду telegram канал StepOne, где оставляю много интересных заметок про коммерческую разработку и мир IT глазами эксперта.
Комментарии (48)

AxisPod
12.09.2022 12:18+2Решение сам и написал. Перечисления в C# - это именно классические перечисления, просто константы на уровне компилятора. В Java это совершенно иная конструкция. Использование enum в Java стиле спорная вещь, по сути это перечисление, что возвращает данные совершенно иного типа. Ну и как бы слегка нарушает принцип единой ответственности.
Ковариантность - это такой неплохой способ стрелять себе в ногу. Зачем? Пример приведён чисто академический. В реальной жизни ООП на таком уровне где-то используется? Лучше уж жить на фабриках и тогда ковариантность вообще не потребуется.
Смотри что такое delegate в C#
Собственно мне наоборот не нравится данная фича. Так как олпять же вносит энтропию в проект. Да, слегка уменьшает объёмы кода, но при этом в Java есть куча всего, что прям увеличивает объёмы кода в неимоверных масштабах. Ну и опять же, делегаты убирают кучу необходимых мест для использования анонимных реализаций интерфейсов.

Stefanio Автор
12.09.2022 12:23+1Про делегаты написал ещё в статье, что их существование аннулирует необходимость функциональных интерфейсов:
В C# есть делегаты, поэтому надобность в подобном сахаре крайне сомнительна

Maccimo
12.09.2022 15:38+5Перечисления в C# — это именно классические перечисления, просто константы на уровне компилятора. В Java это совершенно иная конструкция. Использование enum в Java стиле спорная вещь, по сути это перечисление, что возвращает данные совершенно иного типа.
Дланьчело.
Перечисление это математическая абстракция, представление оного в виде числа — не более, чем его грубое приближение из-за ограниченности ресурсов на заре компьютеризации, а вовсе не "классика".An enumeration is a complete, ordered listing of all the items in a collection. The term is commonly used in mathematics and computer science to refer to a listing of all of the elements of a set.
https://en.wikipedia.org/wiki/Enumeration
Никаких ограничений на внутреннюю структуру элементов перечисления вышеупомянутая абстракция не налагает.

fransua
12.09.2022 12:41-4Не хватает возможности указать исключения, которые может бросать метод.

Neki
12.09.2022 12:51+12Ну вроде все нормальные языки стараются избавиться от проверяемых исключений) А для непроверяемых это делать крайне странно IMHO

amarkevich
12.09.2022 23:11не избавиться вообще, а оставить проверяемые там, где это полезно. наблюдать исключение в рантайме - такое себе занятие, в сравнении с необходимостью обработки на этапе написания кода.

Neki
12.09.2022 23:38Ну вот например kotlin, почитайте там целая глава почему они отказались от проверяемых исключений. https://kotlinlang.ru/docs/exceptions.html
В swift, typescript тоже обошлись без них

FruTb
13.09.2022 01:37+1Тут вся школа ФП-на-эффектах икнула. ZIO/CATS на скале к примеру.
Причем фоннейман - это лямбда-счисление которые доказывается. А тьюринг - недоказуемая модель

Stefanio Автор
12.09.2022 12:55+4Да, компилятор конечно не заставишь ругаться, но можно использовать xmldoc тег
<exception>
https://docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/xmldoc/recommended-tags#exception

onets
12.09.2022 13:043 и 4 пункт мне напомнил дефолтные реализации методов в интерфейсе в c# https://docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods

SadOcean
12.09.2022 13:28+131 пункт по делу - конечно enum как константы быстрые и простые по поведению, но данных в них не хватает.
Возможно это стоит решать не докруткой enum, а заведением честного типа-суммы специально для этих целей.
2 пункт имеет не очень большую практическую ценность, возможно явное указание даже лучше.
Но это на любителя.
А вот 3 и 4 пункты наоборот видятся слабостью Java
В c# очень рано появился хороший механизм делегатов (которые фактически и являются стандартизированным интерфейсом одного метода) и лямбды, по сути полностью исключив "интерфейс с одним методом" и сахар "реализации на месте".
Java пришлось догонять.
Чего еще можно добавить - неизменяемых переменных, аналога val в kotlin
В Java в замыкание можно положить только final переменную, что значительно улучшает понимание замыканий.
Kanut
12.09.2022 16:17Возможно это стоит решать не докруткой enum, а заведением честного типа-суммы специально для этих целей.
По идее есть же Case Guards:
public readonly struct Point { public Point(int x, int y) => (X, Y) = (x, y); public int X { get; } public int Y { get; } } static Point Transform(Point point) => point switch { { X: 0, Y: 0 } => new Point(0, 0), { X: var x, Y: var y } when x < y => new Point(x + y, y), { X: var x, Y: var y } when x > y => new Point(x - y, y), { X: var x, Y: var y } => new Point(2 * x, 2 * y), };То есть берёшь любой класс с произвольными полями и лепишь по нему нужные switch'и. Или я чего-то не понимаю?

ris58h
12.09.2022 17:44+1У вас не тип-сумма в примере, хоть и pattern matching присутствует. Посмотрите примеры в Википедии https://en.m.wikipedia.org/wiki/Tagged_union
В Java enum - это тоже не тип сумма. Для этого там есть sealed classes. У enum в Java просто можно хранить связанные с его значениями данные, что иногда бывает удобно. Связанные со значениями enum, а не с типом, заметьте. Грубо говоря, enum в Java - это final-класс, который нельзя расширить, и несколько его именованных синглтонов. Почти как в примере реализации из статьи.

slonopotamus
15.09.2022 08:32+1Грубо говоря, enum в Java - это final-класс, который нельзя расширить, и несколько его именованных синглтонов.
А вот и нет. Одна из фишек енумов из Java в том что можно объявить метод в енуме, но потом написать разные оверрайды в каждом из элементов.

alaudo
12.09.2022 15:34+1Отличная статья!
По поводу enum, можно использовать Extension Method чтобы сделать что-то подобное. Вот набросал простой пример:void Main() { var today = Week.Monday; today.ShouldIWorkToday().Dump(); } public enum Week { Monday, Tuesday, Wednesday, Thurday, Friday, Saturday, Sunday } public static class WeekHelper { public static bool ShouldIWorkToday(this Week day) => day != Week.Saturday && day != Week.Sunday; }

DistortNeo
12.09.2022 18:13+2Вот уж чего не хватает C#, так это реализации
IEquatable<>для enum.
Вместо этого приходится использовать костыль в видеEqualityComparer<>.Default.Equals(который, кстати, инлайнится, что не так уж и плохо).Насчёт пункта 4: скоро в C# завезут file-local types:
https://github.com/dotnet/csharplang/issues/6011

md_backend_binance
12.09.2022 21:56Мне не хватает
1) наследование enum
2) деконструкторы анонимных обобщенных ограничений очень крутая фича для выдергивания на ходу нужных полей без создания структур
3) взаимодействие микросервисов по REST с помощью типизированных интерфейсов как wcf но лайтовее в миллиард раз
4) использование enum в обобщённых ограничений
4й пункт я как вы знаете протолкнул. 2й пункт признаюсь очень трудно протолкнуть. 3й пункт будет через несколько версий , над этом фичей работаем вместе с командой и MVP MSFT Suzuki, Yoshizaki, Kawai (это всё азиатские имена , не фирмы ^_^)
Обещаю что 3й пункт будет базой для создания взаимодействия микросервисов .net

lair
13.09.2022 00:15+4взаимодействие микросервисов по REST с помощью типизированных интерфейсов как wcf но лайтовее в миллиард раз
А с какой радости это часть языка?
4й пункт я как вы знаете протолкнул.
Неа, не знаем. Где про это почитать?

md_backend_binance
13.09.2022 11:07По enum добавили в 7.3 Можете тут посмотреть https://dotnetfalcon.com/whats-new-in-c-7-3/ пункт "New generic type constraints"
По пункту 3: Часть из за того что уже есть всё что нужно для этого а именно serviceHost и httpclient и interfaces
@Stefanioнет, мы работает и взяли за основу магикюнион, который показывает максимальную производительность и самый низкий лэтенси из всех возможных подобных решений.

lair
13.09.2022 14:15+2По enum добавили в 7.3 Можете тут посмотреть https://dotnetfalcon.com/whats-new-in-c-7-3/ пункт "New generic type constraints"
По этой ссылке нет никакого упоминания, кто это "протолкнул". Где можно про этот процесс прочитать?
По пункту 3: Часть из за того что уже есть всё что нужно для этого а именно serviceHost и httpclient и interfaces
HttpClient — точно не часть языка. Как вы — и, главное, зачем — планируете делать в языке что-то настолько высокоуровневое и специфическое?

xXxVano
12.09.2022 22:50Честно говоря что что, а 2-й пункт никогда не вызывал вопросов. Потому что в отличие от перегрузки виртуального метода, интерфейс можно реализовать явно. Да, это +1 строчка, но имхо это несущественная мелочь.
class Foo : ICloneable { object ICloneable.Clone() => Clone(); public Foo Clone() { throw new NotImplementedException(); } }

TimeCoder
13.09.2022 03:12Описывать enum, и здесь же - задавать некие значения - имхо, не в духе ООП языков. Даже взять ваш пример, некий парсер: в одном файле будет enum, как описание типа, в другом - код самого парсера, где тип используется. Какие символы надо трактовать как некий enum item - это задача и ответственность парсера. Читая его код придётся прыгать внутрь enum чтобы увидеть, какие именно символы соответствуют каждому пункту. А если правило усложнится, его описание может вообще перестать сводиться к строковому литералу. В общем случае, пункты enum могут в разных частях кода иметь разное соответствие каким-то ситуациям, значениям. Это зона ответственности кода, использующего тип. А вы в сам тип пытаетесь частично перенести логику кода, этот тип использующего. В общем случае, не на синтетически примерах, даёт ли это больше плюсов, чем минусов?

a-tk
13.09.2022 08:22Можете не задавать никакие номера и пользоваться только символьными именами. Так сказать, используйте перечисления канонично.

kemsky
13.09.2022 21:45+1Да, какие-то вещи непривычны после джавы, но если говорить только про язык, то скучать нет причин - джава дубовая. Их можно понять, сущестующие кодовые базы огромные. Джава это история компромисов, отсюда и эти functional интерфейсы и стирание типов и много чего еще.
После перехода с джавы мне больше все нехватало и нехватает возможности нормально дебажить сторонний код без сорцов, декомпилировать, и в джаве было куда проще запатчить стороннюю либу.

slonopotamus
15.09.2022 08:39А мне в C# очень не хватает 10-летней поддержки LTS-релизов.

a-tk
15.09.2022 08:57А в чём трудность? Написанное 10 лет назад не работает?

slonopotamus
15.09.2022 09:31Написанное 10 лет назад не работает?
Нет, не работает. Вы не можете запустить приложение сбилженное об 3.1 дотнет имея на руках только шестой рантайм. А 3.1 рантайм через три месяца станет eol.
Кстати, обратной совместимости между рантаймами мне тоже не хватает.
alexeishch
1 - Enum должны быть быстрыми. Они для этого и целочисленные
2 - В Java Generic это синтаксический сахар языка, с точки зрения виртуальной машины IList<ClassA> и IList<ClassB> это одно и то же из-за Type Erasure (https://docs.oracle.com/javase/tutorial/java/generics/erasure.html). В .NET Generic реализованы на уровне виртуальной машины специальными опкодами.
3 - они .NET 7 появятся
4 - в C# для этих целей сделаны делегаты
Ну т.е. не нужно на C# писать также как пишут на Java, это отдельный язык и несмотря на его похожесть - это всё же не то же самое
ris58h
Вы это так формулируете, что может показаться, что в Java enum не быстрый. Что вы, кстати, вкладываете в это понятие?
sla000
Возможно тут речь про то что существует полно статей в которых не рекомендуется использовать enum под Android, дескать размер программы больше в разы а расход памяти больше на порядок по сравнению с public static final int
ryzhehvost
В Java всё не быстрое...
andreyverbin
Дело не в скорости, а в p/Invoke. Изначально enum делался совместимым с C enum
a-tk
Строго говоря нет причин не сделать как с потомками WaitHandle: при p/invoke они передаются как инкапсулируемые хэндлы.