Комментарии 96
С любимчиком и я не определился, уж больно хороши оба. Поэтому надеюсь в комментариях прочесть неизвестные мне ранее аргументы, подкрепленные мнениями сторон. А по каким типам задач вы разделяете применение C# и Kotlin, если не секрет?
Тут скорее зависит от среды в которой будем крутиться, если JVM, то теперь с радостью можно взять Kotlin и не плеваться от Java. Если крутимся на .Net то незачем тащить ещё JVM туда.
В видео с конференций правильно говорят спикеры JetBrains, проще всего работать на Котлине, .Net-разработчику, в моём случае это точно так и есть. Да и сами JetBrains говорили, что многие идеи они смотрели у многих ЯП, но в частности из C# много хорошего взяли и доработали, довели до конца.
Синтаксис Котлина ещё более лаконичен и гибок, это очень нравится. Конструкции инициализаций понравились. Поругать можно в C# nullable, который не так очевиден, статьи уже были.
В Котлине могу поругать lateinit и то в разделе андроид, тут скорее сам андроид виноват, с его заковыристым жизненным циклом, в итоге lateinit уж не такой и поздний и можно упасть.
А так при переключении с C# на Котлин, мне не приходится сильно переключать мозг, почти всё что я мог бы выразить в C# я могу выразить в Котлин, просто библиотечные методы именованы по другому или немножко конструкция другая. Особенности только при использовании коллекций JVM, т.к. они где-то другие и т.п. Ну и async/await конечно в C# очень удобен.
Десктоп явно на C#.
Для кроссплатформенного десктопа на C# есть Avalonia UI и пока только обещают MAUI, а для Kotlin ничуть не худший Jetpack Compose и TornadoFX + все обилие старых Java UI.
Большая часть веба тоже на C#. Бэкенд сейчас разделяется и на том и на том.
Для Kotlin есть весьма легкий в использовании spring boot, новый ktor, ну и конечно опять все Java фреймворки. У C# в этом отношении монополия ASP.NET Core, пусть и очень быстрого.
Со всем остальным пожалуй согласен.
Для кроссплатформенного десктопа на C# есть Avalonia UI и пока только обещают MAUIXamarin.Forms-то никуда не делся. MAUI на его основе делают
По поводу бэкенда, кхм так называемая монополия .Net Core ничем не мешает и руки не связывает, и опять же писал выше от окружения будет зависеть. Лишний раз крутить JVM просто потому что у неё там много фреймворков, такая себя идея. Тут например интегрируем банковское ПО и терминалы, и у них тут на апаче со старой JVM крутится, теперь появилась новая ответственность, ковырять кишки апача, а то всякие интересные ошибки появляются…
До этого интегрировали всяко разное и весовое оборудование и терминалы на андроиде, платы Raspberry, C# Interop работает намного лучше, .Net Core back-end столько нареканий не вызывает.
Для кроссплатформенного десктопа
Кроссплатформенность нужна не всегда, иногда проще и быстрее взять тот же WinForms/WPF/UWP и не особо парится насчет кроссплатформы, если продукт нужный, своих пользователей он найдет как раз таки потому что у большинства конечных юзеров (в большинстве случаев) как раз таки стоит винда.
Впрочем с другой стороны сам рад движению Microsoft в сторону кроссплатформености и та-же Avalonia лично мне очень даже нравится
Спасибо за обзор, но забудьте уже, что
В C# нам доступны значимые (обычно размещаются на стеке) и ссылочные (обычно размещаются в куче) типы.
Честное слово, 9 из 10 собеседуемых отвечают таким образом.
Во-первых, сами типы не размещаются на стеке, речь в лучшем случае должна идти о _локальных переменных_ этих типов. Когда задаешь вопрос, а где тогда располагается значение поля типа int экземпляра класса, примерно треть утверждает, что на стеке, остальные догадываются, что видимо все-таки в куче, хоть оно и значимого типа.
Во-вторых, это деталь реализации рантайма, которая не имеет отношения к языку.
А в третьих, если место аллокации локальных переменных было бы ключевым отличием, то и типы назывались бы кучевыми и стековыми. Но они почему-то называются ссылочными и значимыми. Может быть потому, что ключевое различие в семантике присваивания - либо копированием ссылки_ , либо копированием _значения_?
Во-первых, сами типы не размещаются на стеке, речь в лучшем случае должна идти о _локальных переменных_ этих типов. Когда задаешь вопрос, а где тогда располагается значение поля типа int экземпляра класса, примерно треть утверждает, что на стеке, остальные догадываются, что видимо все-таки в куче, хоть оно и значимого типа.
Да, это конечно так, думал, слова 'обычно' будет достаточно. Уточнил в тексте
Во-вторых, это деталь реализации рантайма, которая не имеет отношения к языку.
На мой взгляд рантайм имеет значение. Теоретически семантика могла бы ложиться на работу с памятью и другим образом, однако по факту мы имеем стек и кучу и работаем с ними. Эта разница легко обнаруживается простым профилированием.
Насколько логично звучит для вас фраза "В русском языке стальные предметы обычно тяжелые, а деревянные — лёгкие"? Вроде бы и правильно по смыслу, а сравнение иголок с бревнами мы проведем по категории не-обычно. Но при чем тут русский язык?
И я не собираюсь переубеждать лично вас, но на таких статьях в том числе учатся люди, которые потом придут к нам на собеседование и будут как заученную мантру повторять "ссылочные типы хранятся на стеке, а значимые в куче". Может быть, прочтя комментарии, они задумаются "хм, а ведь и правда, они же не кучевые и стековые, наверное, неспроста!"
Касательно обучающихся. В бытность преподавания программирования, я объяснял типы на примере мессенджера.
Допустим, я хочу отправить Васе и Оксане видос. Я могу взять где-то файл, отправить два раза его, при этом он будет долго и неприятно ползти по сети до них. А могу залить куда-нибудь на YouTube и отправить ссылку. Это быстро. Пожалуйста, ссылочный тип.
Затем, я хочу отправить им слово «Привет». Если бы у меня был только механизм ссылок, мне пришлось бы где-нибудь размещать слово, крафтить на него ссылку, и отправлять ссылку. Но намного быстрее (и адекватнее, доложу я вам) просто отправить само слово. Пожалуйста, тип-значение.
При этом слово «Привет» может быть и в видосе, но от этого оно (слово) не перестанет храниться по ссылке.
Прошу прощения, я не понял ваш вопрос. Вы не могли бы его переформулировать?
Когда куда-то передаётся значение ссылки, происходит копирование значения ссылки точно так же, как копирование любого другого значимого типа.
В этом смысле значение ссылки ведёт себя точно так же, как значение любого значимого типа, и устраняется разрыв между этими категориями типов.
Все верно, разрыв устраняется, но разработчику нужно понимать разницу между категориями, чтобы представлять, что будет сохранено в x после var x = new Point(); в зависимости от того, объявлен тип Point как структура или как класс. И соответственно, что будет скопировано в y - ссылка (значение ссылки) или значение целиком.
Спасибо за обзор, но забудьте уже, что [...]А как вы предлагаете им отвечать, если это написано на страничке описания типов C#?
Честное слово, 9 из 10 собеседуемых отвечают таким образом.
Value types:
There's no separate heap allocation or garbage collection overhead for value-type variables.Reference types:
When the object is created, the memory is allocated on the managed heap, and the variable holds only a reference to the location of the object.
Во-первых, сами типы не размещаются на стеке, речь в лучшем случае должна идти о _локальных переменных_ этих типов.Позвольте мне также побыть занудой и заметить, что локальные переменные не размещаются на стеке, там размещаются _значения_ локальных переменных ;) Но всё-таки, «типы размещаются на стеке» — это удобное упрощение, как мне кажется.
Когда задаешь вопрос, а где тогда располагается значение поля типа int экземпляра класса, примерно треть утверждает, что на стеке, остальные догадываются, что видимо все-таки в куче, хоть оно и значимого типа.Действительно, догадаться можно, но чтобы поставить собеседуемого в тупик лучше спрашивать про боксинг, и вот тогда процент догадывающихся резко упадёт.
Во-вторых, это деталь реализации рантайма, которая не имеет отношения к языку.Если бы отношения не было, в языке не было бы, например, ключевых слов (либо они имели бы другое назначение)
in
, ref
(в том числе ref struct
), stackalloc
.А в третьих, если место аллокации локальных переменных было бы ключевым отличием, то и типы назывались бы кучевыми и стековыми. Но они почему-то называются ссылочными и значимыми. Может быть потому, что ключевое различие в семантике присваивания — либо копированием ссылки_, либо копированием _значения_?То есть ref int — это ссылочный тип? stackalloc int[] — это значимый тип? А присваивание боксингом куда попадёт?
Всё-таки, на мой взгляд, то, как присваиваются значения — это важное, но всё-таки следствие того, где значения располагаются. Также, например, важным следствием является то, как идёт очистка памяти от значений.
По вашей же ссылке я читаю
A class or a record is a reference type. When an object of the type is created, the variable to which the object is assigned holds only a reference to that memory. When the object reference is assigned to a new variable, the new variable refers to the original object. Changes made through one variable are reflected in the other variable because they both refer to the same data.
A struct is a value type. When a struct is created, the variable to which the struct is assigned holds the struct's actual data. When the struct is assigned to a new variable, it's copied. The new variable and the original variable therefore contain two separate copies of the same data. Changes made to one copy don't affect the other copy.
И только потом идет про кучу.
Действительно, догадаться можно, но чтобы поставить собеседуемого в тупик лучше спрашивать про боксинг, и вот тогда процент догадывающихся резко упадёт.
Кстати, по моей практике, нет. Про boxing/unboxing большинство что-то слышали и с разной степенью уверенности могут ответить, что в случае боксинга значение попадет в кучу.
Всё-таки, на мой взгляд, то, как присваиваются значения — это важное, но всё-таки следствие того, где значения располагаются. Также, например, важным следствием является то, как идёт очистка памяти от значений.
Не могу в полной мере согласиться. Представьте, что в рантайме .NET 8 кроме стека и кучи появилась какая-нибудь "яма", и рантайм будет сам решать, когда выделять память в куче, а когда в яме. Семантика типов-значений и типов-ссылок от этого не изменится — в переменных значимых типов по-прежнему будет храниться само значение, а в переменных ссылочных типов — ссылка на значение, хранящееся в куче или в яме, со всеми вытекающими особенностями присваивания.
Хотя постойте… Зачем представлять? Уже есть Large Object Heap, и рантайм решает за нас, аллоцировать память в SOH или в LOH. И да, разработчику иногда важно представлять, где произойдет аллокация, чтобы оптимизировать производительность, но с точки зрения языка C# разницы между "small" reference type и "large" reference type нет.
По вашей же ссылке я читаюЭм. Во-первых, это в другом разделе, поэтому странно говорить о том, что там «потом», как будто «потом» менее важно. А во-вторых, я же не утверждал обратного. Но вот вы утверждали, что нужно забыть то, что есть в документации.
Кстати, по моей практике, нет. Про boxing/unboxing большинство что-то слышали и с разной степенью уверенности могут ответить, что в случае боксинга значение попадет в кучу.Хм, интересно, конечно. Люди, которые слышали про боксинг, но не слышали о том, что value типы бывают внутри reference) Но не спорю, такое может быть.
Семантика типов-значений и типов-ссылок от этого не изменится.Ну так да. Неважно, где там именно хранятся ссылочные типы. Важно, что их хранение отличается от value типов. Я о чём. Да, то, что там происходит в рантайме — дело прежде всего рантайма, это факт. Но райнтайм .NET и C# не независимы и при помощи упомянутых мной ключевых слов можно напрямую влиять на то, как будет аллоцирована память.
Хотя постойте… Зачем представлять? Уже есть Large Object Heap, и рантайм решает за нас, аллоцировать память в SOH или в LOH.А ещё с .net 5 есть Pinned Object Heap, так что туда добавим, простите, POH. Но это всё — способы оптимизации кучи, уже действительно детали рантайма, и мы на них никак не можем повлиять напрямую. Поэтому:
но с точки зрения языка C# разницы между «small» reference type и «large» reference type нетДа. Но разница между value type и reference type — есть)
Неважно, где там именно хранятся ссылочные типы.
вот тут в целом соглашусь
Важно, что их хранение отличается от value типов.
а вот тут нет. Вообще не существует задачи "хранить данные", есть только задача "получать к данным доступ". Важно то, как эти типы используются и в чем специфика их применения.
Для высокоуровневой логики совершенно фиолетово, где там и что лежит. А "два независимых значения" и "две ссылки на одно и то же значение" - это "две большие разницы"
Вообще не существует задачи «хранить данные», есть только задача «получать к данным доступ».Во-первых, я не говорил, о том, что есть «задача хранить данные».
Во-вторых, иногда она на самом деле есть. Если мне нужен быстрый массив для текущих расчётов, то я скорее создам его через
stackalloc
, потому что мне нужно хранить такие данные на стеке. Пусть такая ситуация редка и возникает уже на этапе оптимизации, но она вполне реальна. В-третьих, а как вы решаете задачу «получать к данным доступ»?
Для высокоуровневой логики совершенно фиолетово, где там и что лежит.Так C# используется не только для высокоуровневой логики.
Позвольте мне также побыть занудой и заметить, что локальные переменные не размещаются на стеке, там размещаются значения локальных переменных ;) Но всё-таки, «типы размещаются на стеке» — это удобное упрощение, как мне кажется.
Можно я тоже побуду занудой? Значения локальных переменных любых типов могут располагаться на стеке; могут располагаться в регистрах процессора; могут располагаться в куче, если на ними создаётся замыкание; могут вообще нигде не располагаться.
Я активно занимаюсь разработкой на Kotlin, а на C# писал только в универе, когда заставляли использовать именно этот язык. Есть ли что-то подобное котлиновским делегатам в C#?
fun main() {
val remoteSource = RemoteSourceImpl()
remoteSource.name = "NameStub"
println("Address: ${remoteSource.address}")
}
interface RemoteSource {
fun get(key: String): String
fun set(key: String, value: String)
}
class RemoteValue(
private val key: String
) : ReadWriteProperty<RemoteSource, String> {
override fun getValue(thisRef: RemoteSource, property: KProperty<*>) =
thisRef.get(key)
override fun setValue(thisRef: RemoteSource, property: KProperty<*>, value: String) =
thisRef.set(key, value)
}
class RemoteSourceImpl : RemoteSource {
var name by RemoteValue("name")
var address by RemoteValue("address")
override fun get(key: String) =
"Value for key $key from remote destination"
override fun set(key: String, value: String) {
//Saving value $value for key $key
}
}
В первом приближении делегат — это указатель на метод статический или экземпляра, а лямбда есть анонимный метод, который, возможно, замыкает контекст через объект:
static class Enumerable
{
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach (var item in source)
{
yield return selector(item);
}
}
}
.
Это ведь просто лямбды. В котлине ведь это другое. В котлине делегат это способ вынести логику свойства объекта в отдельный класс.
class WithoutDelegate {
private var stringValue = "0"
var value: Int
get() = stringValue.toInt()
set(value) {
stringValue = value.toString()
}
}
class WithDelegate {
var value: Int by object: ReadWriteProperty<Any?, Int> {
private var stringValue = "0"
override fun getValue(thisRef: Any?, property: KProperty<*>) =
stringValue.toInt()
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
stringValue = value.toString()
}
}
}
В данном конкретном случае код с делегатом получился запутанней и длиннее. Но он позволяет удобно разносить логику по классам
int DoSomething(Func<int> getValue) => getValue();
var a = DoSomething(x => 42);
Делегат сам по себе и является этой обёрткой, работа с корой идёт на уровне языка, без прочих явных объектов.
Это когда некий класс, согласно своей сигнатуре, реализует некий интерфейс
ISomeInterface
, но методы этого интерфейса отсутствуют в классе, а перенаправляются на объект-реализатор, который является членом класса и реализует тот же ISomeInterface
, но уже явно (в нём присутствуют методы интерфейса).В c# увы, ничего подобного нет. С ключевым словом
delegate
из c# ничего общего.Подскажите пожалуйста, как это можно загуглить?
docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/events
В котлине подобное можно сделать через корутины и Flow. А с помощью перегрузки операторов можно даже и синтаксис +=
использовать
operator fun <T> Flow<T>.plusAssign(collector: suspend (T) -> Unit) {
GlobalScope.launch { collect(collector) }
}
val onClickEvent = MutableSharedFlow<Unit>()
onClickEvent += { println("onClick") }
onClickEvent.tryEmit(Unit)
Ну разве код onClickEvent += { println("onClick") }
не более выразительный, чем делегаты?
Ну разве код onClickEvent += { println(«onClick») } не более выразительный, чем делегаты?
Что?
А вообще
event Action someEvent = delegate{};
someEvent += () => Console.WriteLine();
event Action someEvent;
someEvent += Console.WriteLine;
А да, точно, спасибо. Я на автомате = delegate пишу, чтобы налреф не получить
Зачем проверять на нул, если ?. И так это делает?
А, ясно). По поводу "Считается, что между этими командами кто-то и отписаться может", не думаю что это какая то проблема:
Если отписались, значит так было нужно юзеру
Думаю, очень сложно будет попасть в такой тайминг
Можно навесить lock
Ну и прсто звучит дико (не в обиду)
Раньше надо было в локальную переменную прикапывать, потом вызывать из неё, а теперь достаточно вопросик поставить. Правда красивые скобки заменили на .Invoke, но это не такая большая цена.
Разве вопросик избавляет от гонки? Это же синтаксический сахар
Кстати, += и -= для событий, равно как и Delegate.Combine / Delegate.Remove не thread-safe.
Ну да, за счёт локальной переменной.
Ну а с += в чем проблема?
Да, знаю, но не знаю почему, это меня отталкивает. Скорее всего, из за постоянных нулчеков и лишных символов при написании
События - плохой пример: даже среди команды C# не все в восторге от этой фичи, особенно от механизмов подписки/отписки.
А в чем практическое различие? В котлине лямбды и ссылки на методы взаимозаменяемые:
class Greeter(
private val name: String
) {
fun greet(printer: (String) -> Unit) =
printer("Hello $name!!!")
}
object Writer {
fun write(message: String) = print(message)
}
val greeter = Greeter("World")
greeter.greet { message -> print(message) } //Лямбда
greeter.greet(Writer::write) //Ссылка на функцию
Point p1 = new();
Point p2 = new(1, 2);
В котлине не длиннее:
val p1 = Point()
val p2 = Point(1, 2)
Даже короче, за счет отсутствия точки с запятой
Котлин все таки не скриптовый язык, поэтому такой проблемы нет. А если хочется написать в одну строку, то можно оставить одну точку с запятой: val p1 = Point(); val p2 = Point(1, 2)
Это другое. Он имел ввиду, что в с#
Point p = new();
Создаваемый экземпляр выводится от указанного типа слева
А в случае с#
var p = new Point()
Тип переменной выводится из выражения справа
Это разный подход. Первый удобен для объявления переменных, второй - для выведения типа из сложного выражения, часто из генериков
К плюсам C# я бы отнес тот факт, что и язык, и платформу развивает одна компания, и как следствие у C# гораздо больше возможностей для развития, поскольку в случае необходимости можно и стандартную библиотеку расширить (например, Task, ValueTuple и т.д.) и рантайм поправить (Generics, dynamic)
fun transform(p: Point) = when(p) {
Point(0, 0) -> Point(0, 0)
...
}
Не надо так. Тут при каждом исполнении будет создаваться новый объект. Как бы красиво это не выглядело.
numbers.any {
// объемная логика ...
return calculatedResult
}
Нельзя здесь простой return
, он попытается вас вернуть из текущей функции. Надо просто написать calculatedResult
(или какое-то выражение) в последней строке. Либо return@any calculatedResult
Ну а по существу — синтаксис хвостовых лямбд и inline функции — это и есть самое вкусное в Kotlin, что позволяет создавать DSL.
Func<int> x = () => 1;
Func<int> y = () => 2;
Func<int> z = x + y;
Console.WriteLine(z());
Что будет выведено?
Скорее для тех, кто пришёл из мира Java.
Вообще это очень интересная магия, если посмотреть IL код, видно что компилятор делает следующее:
Func<int> z = (Func<int>) (Delegate) Delegate.Combine(x, y);
Это очень похоже на неудачно выстреливающий костыль в компиляторе: очень похожие вещи происходят в event-ах, но там все выглядит логично из-за наличия в определении ключевого слова ивент которое явно говорит что «сложение» означает последовательный вызов обработчиков:
public event Func<int> Z;
// …
Z = x; // подписать на ивент X
Z += y; // а ещё подписать Y
Похоже что компилятор де-факто не проверяет наличия «event» в определении z - поэтому синтаксис из примера и работает. Интересно было бы копнуть глубже и посмотреть как Roslyn делает парсинг, но лень :)
А в чем костыль? Какой вы ещё видите результат сложения двух функций (заметьте, не вызовов)
Костыль в том, что это преобразование объявлено неявно. Если бы тип Func<T> имел статический метод Func<T> operator +(Func<T> a, Func<T> b) - то было бы более менее логично - мы просто вызываем оператор. Но такого метода нету (его не выдаёт рефлексия, его не показывает ILDasm).
Очень похоже что этот кхм «метод» встроен в сам компилятор - компилятор «знает из коробки» что если пытаются сложить две функции одинакового типа, то надо вызвать Delegate.Combine и скастить результат обратно в функцию - именно это знание я и называю костылём - оно неочевидное и его не видно в исходниках рантайма. Зато, я, кажется, нашёл это место в компиляторе: https://github.com/dotnet/roslyn/search?q=System_Delegate__Combine (файл LocalRewriter_BinaryOperator.cs, строки 175 и 230)
На самом деле это не то что бы костыль, просто оператор оператор реализуется в компиляторе. Компилятору всё равно нужно будет инлайнить этот оператор.
Ну и по спеке оператор должен быть (https://github.com/dotnet/csharpstandard/blob/standard-v5/standard/delegates.md).
Непосредственно в сорцах сам Func<T>
не объявлен как тип. Все его объявление выглядит как public delegate TResult Func<out TResult>();
и слово delegate
заставляет компилятор генерировать нужный тип со всеми нужными методами (даже метод Invoke()).
Но вот то что в рефлекшн-е нет метаданных, наверное неправильно. Хотя не думаю что это кому то мешает.
P.S.: Тоже самое кстати с типом string и его оператором +.
Да, вы правы - я как-то привык ходить в декомилированную реализацию для последних новшеств типа асинк и уже подзабыл что многие другие вещи объявляются неявно.
Иными словами, мне хотелось бы видеть определение Func не как делегат сам по себе, а как обертку над делегатом, которая показывает, что именное происходит внутри. Так, к примеру, делают в с++ - вы можете посмотреть что внутри std::function или std::string - мне почему-то казалось, что с# пошёл похожим путём, но это (очевидно) когнитивная иллюзия.
Нуу… вроде и да, но мне в отладчике не было понятно сходу, каким чудом вызвался Delegate.Compose - вроде бы и логично, но в коде такого нету, без анализа компилятора неочевидно почему ИМЕННО ЭТОТ вызов был сделан
В условном С++ тот же оператор сложения строк определён явно - если вы отлаживаете дебаг версию к нему можно перейти поднявшись по стеку
К тому же, сложение функций в стиле последовательного вызова - это не так чтоб очевидная операция. Я бы лично предпочёл синтаксическую ошибку в таком случае (вызывайте компоуз явно если действительно хотите их “сложить”). Т.е. «сложение» ивент хендлеров мне (лично) выглядит очевидным, а вот сложение «обычных» функций - нет. С#, к сожалению, не разделяет эти два юз кейса.
Считайте что делегат — это тоже встроенный тип.
Руки оторвать за такой код:))
Разница в подходе к работе с null объяснима: Kotlin изначально проектировался с ненулевыми объектами, а у C# к моменту появления nullable reference types было 18 лет легаси-кода. Задача оператора !
— объяснить компилятору, что вы здесь всё проверили, null на практике здесь не будет, хотя чисто формально это не так.
Хотелось бы увидеть сравнение async/await и корутин. Подходы очень разные, подводных камней при переключении между языками много.
Если честно, я не очень понял как это работает в Котлин.
Вот пример: допустим у нас есть класс Service1, который создаётся IoC контейнером, он зависит от логгера, который инжектится тем же IOC контейнером, в с# я могу написать как-то так:
class Service1 {
[Inject]
public ILogger Logger { get; set; } = null!;
public void DoSomething() {
// …
Logger.Info(“test”); // Logger is not-null
}
}
Здесь конструкция «= null!» говорит компилятору, что я хорошо подумал, и Logger будет гарантированно инициализирован при создании объекта. Как тоже самое сделать в Котлин?
В котлине для подобного есть lateinit
class Service1 {
@Inject
lateinit var logger: ILogger
fun doSomething() {
logger.info("test")
}
}
P.S. Очень странно в C# выглядит выражение null!
, то есть это null, но мы уверены, что это не null
Да, это понятно. Просто обычно восклицательный знак ставится после выражения для которого мы уверены, что оно не null
Это исторически сложилось так, возможно со временем его научат учитывать атрибуты типа [Inject] - это в целом делабельно (не требует каких-то кардинальных изменений языка). Нечто подобное уже есть: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis
Для структур же использование new вообще противоречит логике, ведь они целиком соответствуют автоматическим объектам не в куче! То есть, new здесь стоит с точки зрения С++ просто ошибочно. Надо было создателям С# делать всё без new как работа со структурами.
Такой подход показывает близость конструкторов к функциям и поддерживается современной концепцией функционального программирования когда функция и конструктор совпадают.
Поэтому я советую создателям Рослина сделать new факультативным. Для меня так и осталась загадка зачем создатели С# пошли таким противоречивым путем впихнув new везде, даже там где его нет типа структур, он вообще нигде не нужен.
Ну вот, например, где это (уже) уместно:
SomeType value = new();
А вообще, несколько избыточный new
-- меньшая из проблем в языке. Не так уж и сильно засоряет код, учитывая повсеместный DI, а когда появляется много манки-кодинга с кучей new
, тут проблема скорее всего не в new
.
C# vs Kotlin