Как стать автором
Обновить

Комментарии 96

Использую на работе оба языка. Отталкиваюсь от задачи, оба ЯП нравятся, очевидного любимчика бы не выделял.

С любимчиком и я не определился, уж больно хороши оба. Поэтому надеюсь в комментариях прочесть неизвестные мне ранее аргументы, подкрепленные мнениями сторон. А по каким типам задач вы разделяете применение C# и Kotlin, если не секрет?

Десктоп явно на C#. Большая часть веба тоже на C#. Бэкенд сейчас разделяется и на том и на том. На андроиде/iOS есть приложение написанное на Xamarin, недавнее новое приложение только для андроида делал на Котлине, приложение должно было быть без сайд эффектов, нативным, быстрым, лёгким. Мелкие проекты можно на любом из них.
Тут скорее зависит от среды в которой будем крутиться, если 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 и пока только обещают MAUI
Xamarin.Forms-то никуда не делся. MAUI на его основе делают
Я в курсе что есть Авалония и т.д. и т.п. Насчёт UI на Java, ну уж такое. Если новый проект будет с UI то можно будет взять и Авалонию и смотришь там MAUI, но я описывал конкретно текущее положение у себя на работе.
По поводу бэкенда, кхм так называемая монополия .Net Core ничем не мешает и руки не связывает, и опять же писал выше от окружения будет зависеть. Лишний раз крутить JVM просто потому что у неё там много фреймворков, такая себя идея. Тут например интегрируем банковское ПО и терминалы, и у них тут на апаче со старой JVM крутится, теперь появилась новая ответственность, ковырять кишки апача, а то всякие интересные ошибки появляются…
До этого интегрировали всяко разное и весовое оборудование и терминалы на андроиде, платы Raspberry, C# Interop работает намного лучше, .Net Core back-end столько нареканий не вызывает.
Для кроссплатформенного десктопа

Кроссплатформенность нужна не всегда, иногда проще и быстрее взять тот же WinForms/WPF/UWP и не особо парится насчет кроссплатформы, если продукт нужный, своих пользователей он найдет как раз таки потому что у большинства конечных юзеров (в большинстве случаев) как раз таки стоит винда.
Впрочем с другой стороны сам рад движению Microsoft в сторону кроссплатформености и та-же Avalonia лично мне очень даже нравится

Спасибо за обзор, но забудьте уже, что

В C# нам доступны значимые (обычно размещаются на стеке) и ссылочные (обычно размещаются в куче) типы.

Честное слово, 9 из 10 собеседуемых отвечают таким образом.

Во-первых, сами типы не размещаются на стеке, речь в лучшем случае должна идти о _локальных переменных_ этих типов. Когда задаешь вопрос, а где тогда располагается значение поля типа int экземпляра класса, примерно треть утверждает, что на стеке, остальные догадываются, что видимо все-таки в куче, хоть оно и значимого типа.

Во-вторых, это деталь реализации рантайма, которая не имеет отношения к языку.

А в третьих, если место аллокации локальных переменных было бы ключевым отличием, то и типы назывались бы кучевыми и стековыми. Но они почему-то называются ссылочными и значимыми. Может быть потому, что ключевое различие в семантике присваивания - либо копированием ссылки_ , либо копированием _значения_?

Во-первых, сами типы не размещаются на стеке, речь в лучшем случае должна идти о _локальных переменных_ этих типов. Когда задаешь вопрос, а где тогда располагается значение поля типа int экземпляра класса, примерно треть утверждает, что на стеке, остальные догадываются, что видимо все-таки в куче, хоть оно и значимого типа.

Да, это конечно так, думал, слова 'обычно' будет достаточно. Уточнил в тексте


Во-вторых, это деталь реализации рантайма, которая не имеет отношения к языку.

На мой взгляд рантайм имеет значение. Теоретически семантика могла бы ложиться на работу с памятью и другим образом, однако по факту мы имеем стек и кучу и работаем с ними. Эта разница легко обнаруживается простым профилированием.

Насколько логично звучит для вас фраза "В русском языке стальные предметы обычно тяжелые, а деревянные — лёгкие"? Вроде бы и правильно по смыслу, а сравнение иголок с бревнами мы проведем по категории не-обычно. Но при чем тут русский язык?


И я не собираюсь переубеждать лично вас, но на таких статьях в том числе учатся люди, которые потом придут к нам на собеседование и будут как заученную мантру повторять "ссылочные типы хранятся на стеке, а значимые в куче". Может быть, прочтя комментарии, они задумаются "хм, а ведь и правда, они же не кучевые и стековые, наверное, неспроста!"

Я могу вам точно сказать, что ref struct хранится в куче. Официально «кучевый тип»? )

Касательно обучающихся. В бытность преподавания программирования, я объяснял типы на примере мессенджера.

Допустим, я хочу отправить Васе и Оксане видос. Я могу взять где-то файл, отправить два раза его, при этом он будет долго и неприятно ползти по сети до них. А могу залить куда-нибудь на YouTube и отправить ссылку. Это быстро. Пожалуйста, ссылочный тип.

Затем, я хочу отправить им слово «Привет». Если бы у меня был только механизм ссылок, мне пришлось бы где-нибудь размещать слово, крафтить на него ссылку, и отправлять ссылку. Но намного быстрее (и адекватнее, доложу я вам) просто отправить само слово. Пожалуйста, тип-значение.

При этом слово «Привет» может быть и в видосе, но от этого оно (слово) не перестанет храниться по ссылке.
Неа, всё ровно наоборот. ref struct как раз не должны попасть в кучу.
Да, да, оговорился. Имел в виду стек и, следовательно «стековый тип». Поздно увидел. «Кучевой» звучит лучше) Как облачко.
А что не так с копированием значения ссылки?

Прошу прощения, я не понял ваш вопрос. Вы не могли бы его переформулировать?

Ссылка тоже имеет значение — текущий адрес целевого объекта.
Когда куда-то передаётся значение ссылки, происходит копирование значения ссылки точно так же, как копирование любого другого значимого типа.
В этом смысле значение ссылки ведёт себя точно так же, как значение любого значимого типа, и устраняется разрыв между этими категориями типов.

Все верно, разрыв устраняется, но разработчику нужно понимать разницу между категориями, чтобы представлять, что будет сохранено в x после var x = new Point(); в зависимости от того, объявлен тип Point как структура или как класс. И соответственно, что будет скопировано в y - ссылка (значение ссылки) или значение целиком.

О, один из моих любимых холиваров. Постараюсь язвить поменьше, если что — заранее извиняюсь.
Спасибо за обзор, но забудьте уже, что [...]
Честное слово, 9 из 10 собеседуемых отвечают таким образом.
А как вы предлагаете им отвечать, если это написано на страничке описания типов C#?

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
    }

}
Есть свои делегаты и события, которые не анонимные классы-реализации интерфейсов, а first class citizens.
В первом приближении делегат — это указатель на метод статический или экземпляра, а лямбда есть анонимный метод, который, возможно, замыкает контекст через объект:

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()
        }

    }

}

В данном конкретном случае код с делегатом получился запутанней и длиннее. Но он позволяет удобно разносить логику по классам

В C# делегат — это указание вызвать вон тот метод без заворачивания в интерфейсы и классы:
int DoSomething(Func<int> getValue) => getValue();

var a = DoSomething(x => 42);

Делегат сам по себе и является этой обёрткой, работа с корой идёт на уровне языка, без прочих явных объектов.

То же самое можно сделать и лямбдами:


val doSomething = { getValue: () -> Int -> getValue() }
val a = doSomething { 42 }
Я так понял, это аналог implements из Delphi.

Это когда некий класс, согласно своей сигнатуре, реализует некий интерфейс ISomeInterface, но методы этого интерфейса отсутствуют в классе, а перенаправляются на объект-реализатор, который является членом класса и реализует тот же ISomeInterface, но уже явно (в нём присутствуют методы интерфейса).

В c# увы, ничего подобного нет. С ключевым словом delegate из c# ничего общего.

Подскажите пожалуйста, как это можно загуглить?

И в продолжение ещё один механизм, который базируется на делегатах и прямого аналога которого нету в Java-мире:
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();

А да, точно, спасибо. Я на автомате = delegate пишу, чтобы налреф не получить

someEvent?.Invoke() и все дела
Ну этот код не сильно отличается от if(someEvent != null) someEvent?.Invoke(); Считается, что между этими командами кто-то и отписаться может. В этом плане присвоение делегата надёжнее.

Зачем проверять на нул, если ?. И так это делает?

А, ясно). По поводу "Считается, что между этими командами кто-то и отписаться может", не думаю что это какая то проблема:

  1. Если отписались, значит так было нужно юзеру

  2. Думаю, очень сложно будет попасть в такой тайминг

  3. Можно навесить lock

  4. Ну и прсто звучит дико (не в обиду)

ну, мне тоже эта ситуация кажется маловероятной. Городить лок для этого тоже монструозно. Вариант с делегатом решает эту проблему изящнее и мне нравится больше всего.
Вы просто в проде гонки не ловили. Бывает больно. А ведь решение проблемы практически ничего теперь не стоит.
Раньше надо было в локальную переменную прикапывать, потом вызывать из неё, а теперь достаточно вопросик поставить. Правда красивые скобки заменили на .Invoke, но это не такая большая цена.

Разве вопросик избавляет от гонки? Это же синтаксический сахар

Избавляет.

Кстати, += и -= для событий, равно как и Delegate.Combine / Delegate.Remove не thread-safe.

Ну да, за счёт локальной переменной.

Ну а с += в чем проблема?

Потому что операция не атомарная.
А копирование ссылки — атомарная.

Да, знаю, но не знаю почему, это меня отталкивает. Скорее всего, из за постоянных нулчеков и лишных символов при написании

События - плохой пример: даже среди команды C# не все в восторге от этой фичи, особенно от механизмов подписки/отписки.

Скорее от отписки.
ConditionalWeakTable для возможности подставить костыль появилась, но этот костыль надо ещё руками вкорячить туда.

Ну Мэдс Торгерсен не мелочился: ему и цепочка делегатов внутри нравится и поведение событий при возникновении исключений...

А в чем практическое различие? В котлине лямбды и ссылки на методы взаимозаменяемые:


Лямбда и ссылка на метод
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) //Ссылка на функцию
Делегаты в C# «из коробки» могут указывать на список методов, что даёт побочным эффектом возможность многократно подписаться на событие.

В котлине можно написать маленький экстеншен


operator fun <T> ((T) -> Unit).plus(
    other: (T) -> Unit
): (T) -> Unit = { param ->
    this(param)
    other(param)
}

И складывать лямбды по всему проекту

На каждый вариант сигнатуры?

Ну да. Вряд ли нам понадобится складывать лямбды с количеством параметров больше 3-х, а если понадобится, то, похоже, пора такой сложный код рефакторить.

В шарпе теперь еще вот так можно
Point p1 = new();
Point p2 = new(1, 2);

В котлине не длиннее:


val p1 = Point()
val p2 = Point(1, 2)

Даже короче, за счет отсутствия точки с запятой

Чем Вам точка с запятой не угадила? А как он отработает, если убрать перенос строки? в один ряд записать? я почему спрашиваю — Мне приходилось писать код и компилировать его на ходу, и я просто ставил разделитель с точкой запятой и не переживал что если что даже съедет — то не так отработает. Как тут?

Котлин все таки не скриптовый язык, поэтому такой проблемы нет. А если хочется написать в одну строку, то можно оставить одну точку с запятой: val p1 = Point(); val p2 = Point(1, 2)

Мне кажется вот такой пример может быть очень даже неожиданностью, для пришедших из других языков. Особенно если не из IDE смотреть, а например на ревью:


fun sum() : Int {
    return 1 + 2 + 3 + 4 
        + 5 + 6
}

Это другое. Он имел ввиду, что в с#

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 и его оператором +.

Можете посмотреть на реализацию System.Int32 например. Там тоже много компиляторной магии, но к ним претензий вроде как нету.
Такие костыли интринсиками зовутся.

Да, вы правы - я как-то привык ходить в декомилированную реализацию для последних новшеств типа асинк и уже подзабыл что многие другие вещи объявляются неявно.

Иными словами, мне хотелось бы видеть определение Func не как делегат сам по себе, а как обертку над делегатом, которая показывает, что именное происходит внутри. Так, к примеру, делают в с++ - вы можете посмотреть что внутри std::function или std::string - мне почему-то казалось, что с# пошёл похожим путём, но это (очевидно) когнитивная иллюзия.

Ну почему же, всё именно так, как Вы говорите и в C#: в отладчике Вы можете видеть и target, и MethodInfo. Просто обычно скрывается такая деталь, как промежуточный наследник от делегата.

Нуу… вроде и да, но мне в отладчике не было понятно сходу, каким чудом вызвался Delegate.Compose - вроде бы и логично, но в коде такого нету, без анализа компилятора неочевидно почему ИМЕННО ЭТОТ вызов был сделан

В условном С++ тот же оператор сложения строк определён явно - если вы отлаживаете дебаг версию к нему можно перейти поднявшись по стеку

К тому же, сложение функций в стиле последовательного вызова - это не так чтоб очевидная операция. Я бы лично предпочёл синтаксическую ошибку в таком случае (вызывайте компоуз явно если действительно хотите их “сложить”). Т.е. «сложение» ивент хендлеров мне (лично) выглядит очевидным, а вот сложение «обычных» функций - нет. С#, к сожалению, не разделяет эти два юз кейса.

А как насчёт оператора сложения у встроенных типов?
Считайте что делегат — это тоже встроенный тип.
А Вы не лезьте в потроха компилятора и читайте спецификацию языка.

Руки оторвать за такой код:))

Это к разработчикам языка, которые разрешили такой синтаксис.

Логично было бы разрешить «сложение» только void-делегатов: там понятно, что они вызовутся последовательно. А если возвращаются значения, то складывать запретить, т.к. значения, кроме последнего, теряются и это не очевидно.

то, что значения теряются, ещё не повод запрещать последовательный вызов

Если очень надо, делай void-обёртку, и только так добавляй свою ф-цию в цепочку обработчиков. Синтаксически это несколько закорючек, зато ясности прибавляет, что именно тут происходит.

Разница в подходе к работе с 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, но мы говорим компилятору, что знаем что делаем.

Да, это понятно. Просто обычно восклицательный знак ставится после выражения для которого мы уверены, что оно не null

Это исторически сложилось так, возможно со временем его научат учитывать атрибуты типа [Inject] - это в целом делабельно (не требует каких-то кардинальных изменений языка). Нечто подобное уже есть: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis

В С# когда создаешь много данных от new в глазах рябит, он только захламляет текст. Не понимаю зачем он был сохранен при переходе от С++. В С++ там понятно, есть создание объекта в куче, а есть автоматически на стеке тогда не надо указывать new и он не захламляет текст. В С# все объекты создаются в куче, но они все автоматические, то есть к ним можно было подойти как в С++ без new.

Для структур же использование new вообще противоречит логике, ведь они целиком соответствуют автоматическим объектам не в куче! То есть, new здесь стоит с точки зрения С++ просто ошибочно. Надо было создателям С# делать всё без new как работа со структурами.

Такой подход показывает близость конструкторов к функциям и поддерживается современной концепцией функционального программирования когда функция и конструктор совпадают.

Поэтому я советую создателям Рослина сделать new факультативным. Для меня так и осталась загадка зачем создатели С# пошли таким противоречивым путем впихнув new везде, даже там где его нет типа структур, он вообще нигде не нужен.
Могу также добавить что в синтаксическом сахаре кортежей отказались от new, что показывает что это правильно. Ведь пишется скажем return (a,b), в не return new (a,b) как было бы положено по старому для структур ValueTuple<>.

Ну вот, например, где это (уже) уместно:

SomeType value = new();

А вообще, несколько избыточный new -- меньшая из проблем в языке. Не так уж и сильно засоряет код, учитывая повсеместный DI, а когда появляется много манки-кодинга с кучей new, тут проблема скорее всего не в new.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории