Спор "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 они передаются как инкапсулируемые хэндлы.