Конкретно для этого случая — скорее, нужно иметь метод Character.TakeDamage(points), который всем этим занимается.
Но само выражение Health = (Health - damage).ButNotLess(than: 0) где-то написать придётся. Кстати, то же самое касается, например, восстановления здоровья. На мой взгляд, запись вида: Health = Min(of: Health + heal, MaxHealth) заставляет задуматься, а вот: Health = (Health + heal).ButNotGreater(than: MaxHealth) — совсем fluent.
В вашем стиле получается, что нужно писать hero.Take(damage) вместо hero.TakeDamage(damage). Но как тогда отличать Take(damage) от Take(pen)?
Альтернатив много: character.Take(damage) и character.Inventory.Put(item), character.Take(damage) и character.Pickup(item) и т.д.
Но в играх предметная область слишком сложна, чтобы можно было ограничиться такими простыми выражениями. Скорее всего, здоровье придётся менять напрямую с помощью character.Health.Change(by: -damage), ибо есть промах, крит. удар, броня, эффекты, и т.д.
Если совсем не избежать, то дублирование по имени параметра hero.TakeDamage(damage) не та уж страшно (хотя подумать, как избежать, точно стоит), как, скажем, Directory.CreateDirectory (но почему-то Directory.Delete). Они качественно отличаются.
Во-первых, теряется единообразие
Единообразие теряется только в каких-то отдельных случаях, но и этим можно пожертвовать. Общая идея методов расширений, как я уже писал, не просто в синтаксисе, а и в семантике того, что получается в записи: у статического метода появляется субъект. Например: char.IsDigit(c) не то же самое, что c.IsDigit().
Во-вторых, нарушается пресловутая идиоматика "операции без побочных эффектов — в статические методы, изменение состояния — в методы объекта"
Впервые слышу об этой идиоматике. Что она постулирует и чем чревато её нарушение?
Насчёт Max тоже согласен с тем, что он, вероятно, лучше всего подходит для большинства случаев, особенно в записи вида: Max(of: a, b).
Но иногда получается вот как: скажем, если минимум здоровья — 0, то мы пишем при этом Health = Max(of: Health - Damage, 0), что, на мой взгляд, именно в этом конкретном случае не так интуитивно, как хотелось бы. Поэтому Health = (Health - Damage).ButNotLess(than: 0) как будто лучше передаёт идею.
В языке нельзя вставлять аргумент посередине имени оператора — придумаем .Without("t").AtEnd.
Но почему нет?
Ну чем оно принципиально лучше s.RemoveSubstring("suffix", fromEnd: true, count: 1)
Давайте отойдём на секунду от x.Without(y).AtEnd, и посмотрим только на предложенное s.RemoveSubstring("suffix", fromEnd: true, count: 1) с точки зрения понятности и лаконичности.
Remove хуже, потому что имеется ввиду не Remove, а WithRemoved. Нужно что-то, что покажет, что результирующая строка — это исходная без какой-то подстроки.
Substring — как мне кажется, лишнее. Удаляя что-то из строки, мы итак понимаем, что это что-то — подстрока (substring).
count: 1 — что-то новенькое. Опустим, как поведение по умолчанию.
Пока получается s.Without("suffix", fromEnd: true). Но что значит тогда: s.Without("suffix", fromEnd: false)? Вообще везде или на старте?
Можно было бы: s.Without("suffix", From.End) или s.Without("suffix", From.Start), но тогда перечисление From получается слишком общего назначения.
Хотя вот ещё: s.Without("suffix", from: End), где End — значение перечисления с более конкретным именем, например, PositionKind. Его импортируем через using static, и готово. Но это попросту неудобно, каждый раз писать и импортировать.
Можно через лямбды: s.Without("suffix", _ => _.FromEnd), s.Without("suffix", _ => _.FromStart). Но это тоже не очень удобно и громоздко.
s.Without("suffix", from: "end") — упираемся в возможность ошибиться при вводе "end".
Вероятно, лучше всего использовать совет от qw1 и взять s.WithoutSuffix("Builder"), s.WithoutPrefix("Abstract") и s.Without("part") как замены предложенным мной вариантам с цепочками.
Да, ещё можно остановиться на s.RemoveFromStart("suffix"). Как можно остановиться на Directory.CreateDirectory или QueueUserWorkItem.
Но, на мой взгляд, мы как раз делаем то, что нужно: размышляем на тему того, как подобрать однозначное и читаемое название, используя ограничения того языка, который имеется.
А то, что язык искусственно созданный и формализованный — не значит, что там нет каких-то своих традиций и идиом. Тащить туда конструкции естественного языка "чтобы было удобнее читать" — это делать какую-то смесь французского с нижегородским.
К счастью, традиции и идиомы устаревают. И уже сегодня написано огромное количество fluent-библиотек (хотя не все они Fluent). Скажем, FluentAssertions и NSubstitute.
a = x.write
b = a.format("json")
c = b.save("...")
И утверждать, что ребята полезли не в тот язык не с той концепцией? Боюсь, нет.
А это вот We.Can(Write.CallChainsThatLook(like: "real sentences")) — по выражению Кристиана Шафмайстера, по сравнению с макросами Лиспа — что налоговая декларация по сравнению с сонетами Шекспира.
Сравнение броское, но, как это часто бывает со сравнениями, отвлекает от сути. Чья-то действительность — C# или Java. В которых можно писать красивее, элегантнее, проще и опрятнее, чем GetUserById или UtilsManager.
В итоге, делаются промежуточные объекты, бесполезные сами по себе.
А как быть, например, с FluentAssertions, которые позволяют:
// И так.
instance.Should().BeNotNull();
// И этак:
instance.Should().Should().Should().Should();
// И даже так:
var x = instance.Should();
var y = x.BeNull();
Как видите, не имеет значения, что объекты могут быть бесполезными сами по себе. Когда вы знаете грамматику, вам и в голову не придёт коверкать её только потому, что это возможно.
А раз так, то мне лично было бы уже всё равно — это будет s.Without(".exe").AtEnd, s.RemoveSubstringFromEnd(".exe") или StringTools.RemoveSubstringFromEnd(".exe", s)
Интересно, как из того факта, что "писать приходится на формальном языке" вырастает StringTools? Почему его не нужно всегда уточнять, а AtEnd нужно? Как отменяется сама форма естественного языка: субъект предложения, объект? Почему из невозможности написать совсем понятно, следует то, что нужно писать совсем непонятно?
Так или иначе, на мой взгляд, обсуждение одного примера (когда их было много и с разных сторон) в разрезе целой идеи — это некорректно.
Да, среди множества способов улучшения читаемости, существуют такие, которые поддерживаются языком или недостаточно полно, или совсем частично. Но из этого, как мне кажется, не следует, что:
а) так писать не нужно;
b) нужен другой язык;
c) нужно писать Tools и Utils.
В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).
Кстати, верно, этого я не знал, прошу прощения!
Тем не менее, сравните сложность слов Remove и Suffix и Without и End (At мы в расчёт не берём). В первом случае вам нужно привлекать специалиста по computer science, а во втором — не обязательно. При этом мы, разумеется, ещё учитываем почти полную тождественность Without(x).AtEnd тому, как это произносится в речи.
Вот то самое стремление к минимальной сложности (т.е. к упрощению) и получается.
Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано fileName.Without(".exe").AtEnd — ребус, а вот если скажет заказчик: "Here I'll need to show file name without .exe at end, can you do that?", то вопросов нет?
Кстати, если "show file name without extension", то сразу: fileName.WithoutExtension() (что предполагает AtEnd).
fileName.RemoveSuffix(".exe")
Но fileName не меняется после вызова Remove. Кроме того, суффикс — это понятие слова, а не целой строки.
Писать понятный и читаемый код — это не новый стиль.
Ещё лет двадцать назад Гради Буч в книге "Object Oriented Analysis and Design with Applications" цитировал другую работу 1989 года (Lins, C. 1989. A First Look at Literate Programming. Structured Programming.) — "Software should be written as carefully as English prose, with consideration given to the reader as well as to the computer".
Ему же приписывают: "Clean code reads like well-written prose".
Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.
На мой взгляд, разница утрирована. Куда точнее: "Все вокруг пишут философские трактаты, а нужно, оказывается, писать понятно и просто? Зачем?".
Стиль мышления технаря против стиля мышления гуманитария.
Мне кажется, это придуманное, а не фактическое положение вещей.
Что технического в CopyUtil?
Нет, это класс, выполняющий копирование
Это и есть копирование.
В отрыве от имени класса вообще бред.
Так в этом и суть: сущности не самодостаточны, а читаются и используются. Как технари мы боремся против сложности и избыточности, стремимся к простоте и понятности.
Мало написать и реализовать алгоритм — это ничто по сравнению с тем, чтобы написать его доступным для других.
Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».
Не уверен, что утверждал именно это, предлагая x.Without(y).AtEnd.
В первом случае больше когнитивная нагрузка, потому что есть два вызова функции, и я должен держать в уме то, что в этой цепочке создаётся искусственный объект, необходимый только для того, чтобы вся эта синтаксическая магия работала.
Если смотреть на это с такой точки зрения, то да. Поэтому я и предлагаю совершить ментальный скачок (в статьях описывая его суть): смотреть на код не только как на объекты, методы, вызовы, свойства, а как на выражения и предложения.
С такой точки зрения "Name_postfix".Without("_postfix").AtEnd читается ровно так, как вы бы ожидали услышать в естественной речи. Или, скажем:
// На мой взгляд, вполне очевидно, что будет в переменной `x`.
var x = "D:\\git\\Repository".AllAfter("git\\");
Это может показаться магией, но по тем же причинам магией может показаться управление памятью или даже сама платформа .NET. И тем не менее, мы обобщаем штуки, чтобы писать более красивые и удобные штуки, чтобы потом, обобщая их, писать кое-что ещё лучше.
Лучше бы написали, как такое реализовывать и не запутаться в простынях кода.
Это размазало бы статью. Особенно если учесть, что реализовать такое нехитро, речь ведь о форме и именах, а не алгоритмах.
писать такое тяжко
Не уверен. Как правило, так же, как и всё остальное. Что непривычно кому-то, кто всю жизнь писал GetUserById, — это верно.
если писать такое в core-классах, возникает overhead на создание промежуточных классов
Если вы про создание в рантайме, то overhead'а почти всегда нет, ведь создаются структуры. Если про написание кода, то да, определённый overhead появляется ввиду того, что в языке C# нет удобных способов реализовывать однозначные цепочки запросов.
А между прочим, какое-то время назад тут было интервью с экспертом по перфомансу в .NET, который советовал не париться насчёт производительности бизнес-логики, которую всё равно пишут люди разной квалификации, но вот core, common, util и т.п. серьёзно оптимизировать (вплоть до отказа от linq), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.
Большая часть материала — про имена и названия (которые определяют содержание), так что на производительность они никак не влияют.
В вопросах производительности контекст первичен. Иначе разработчики платформы .NET не написали бы LINQ, следуя указанным выше критериям.
Иными словами, если красивый код чего-то стоит, пусть стоит, не вызывайте его в while (true).
В моём опыте в основном было так: люди считают замыкания и упаковки, не понимая количество вызовов и общий трафик; избавляются от LINQ (пусть и в Common), хотя общая тенденция проектов такая, что 99% кода не влияют на производительность. Всё это излишне. Как правило, и так понятно: "О, вот тут что-то мне надо рекурсивно будет считать, теоретический масштаб вот какой, тогда, пожалуй, проверю, как оно себя ведёт, и подумаю, заменить ли List на HashSet".
Тут консистентность наименований во всех классах важнее, чем экономия на одном простом классе.
Мне так не кажется. Простой и читаемый код важнее, чем внутренняя согласованность библиотеки. Но речь, разумеется, не про Microsoft конкретно, а про общий подход.
Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.
Зачем так, если можно хотя бы Directories.Of(path) и Files.Of(path)? Глагол не нужен совершенно. При этом лучше вообще использовать методы расширений, чтобы передать "субъектный" оттенок: path.AsDirectory().Files и path.AsDirectory().Directories. Тогда и расширяемость выше.
Потому что это неверно. Метод не ищет свободный поток.
Это потому что вы знаете, как устроен ThreadPool внутри. А метод, меж тем, не должен сообщать, что он делает внутри.
Мне, как клиенту ThreadPool.QueueUserWorkItem совершенно безразлично, будет там внутри очередь или Scheduler. У меня есть действие, и я хочу выполнить его на потоке из пула. Поэтому: "Пул, подыщи-ка мне поток, и выполни эту задачу" — вот вам и FindFreeThreadAndExecuteUserWorkItem получился.
Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).
Если прочитать толькоFind, но там есть ещё AndExecute.
В общем, на мой взгляд, всё это несущественные споры. Основная претензия была не Queue (хотя и к нему есть), а к UserWorkItem.
Но само выражение
Health = (Health - damage).ButNotLess(than: 0)
где-то написать придётся. Кстати, то же самое касается, например, восстановления здоровья. На мой взгляд, запись вида:Health = Min(of: Health + heal, MaxHealth)
заставляет задуматься, а вот:Health = (Health + heal).ButNotGreater(than: MaxHealth)
— совсем fluent.Альтернатив много:
character.Take(damage)
иcharacter.Inventory.Put(item)
,character.Take(damage)
иcharacter.Pickup(item)
и т.д.Но в играх предметная область слишком сложна, чтобы можно было ограничиться такими простыми выражениями. Скорее всего, здоровье придётся менять напрямую с помощью
character.Health.Change(by: -damage)
, ибо есть промах, крит. удар, броня, эффекты, и т.д.Если совсем не избежать, то дублирование по имени параметра
hero.TakeDamage(damage)
не та уж страшно (хотя подумать, как избежать, точно стоит), как, скажем,Directory.CreateDirectory
(но почему-тоDirectory.Delete
). Они качественно отличаются.Единообразие теряется только в каких-то отдельных случаях, но и этим можно пожертвовать. Общая идея методов расширений, как я уже писал, не просто в синтаксисе, а и в семантике того, что получается в записи: у статического метода появляется субъект. Например:
char.IsDigit(c)
не то же самое, чтоc.IsDigit()
.Впервые слышу об этой идиоматике. Что она постулирует и чем чревато её нарушение?
Насчёт
Max
тоже согласен с тем, что он, вероятно, лучше всего подходит для большинства случаев, особенно в записи вида:Max(of: a, b)
.Но иногда получается вот как: скажем, если минимум здоровья —
0
, то мы пишем при этомHealth = Max(of: Health - Damage, 0)
, что, на мой взгляд, именно в этом конкретном случае не так интуитивно, как хотелось бы. ПоэтомуHealth = (Health - Damage).ButNotLess(than: 0)
как будто лучше передаёт идею.Но почему нет?
Давайте отойдём на секунду от
x.Without(y).AtEnd
, и посмотрим только на предложенноеs.RemoveSubstring("suffix", fromEnd: true, count: 1)
с точки зрения понятности и лаконичности.Remove
хуже, потому что имеется ввиду неRemove
, аWithRemoved
. Нужно что-то, что покажет, что результирующая строка — это исходная без какой-то подстроки.Substring
— как мне кажется, лишнее. Удаляя что-то из строки, мы итак понимаем, что это что-то — подстрока (substring).count: 1
— что-то новенькое. Опустим, как поведение по умолчанию.Пока получается
s.Without("suffix", fromEnd: true)
. Но что значит тогда:s.Without("suffix", fromEnd: false)
? Вообще везде или на старте?Можно было бы:
s.Without("suffix", From.End)
илиs.Without("suffix", From.Start)
, но тогда перечислениеFrom
получается слишком общего назначения.Хотя вот ещё:
s.Without("suffix", from: End)
, гдеEnd
— значение перечисления с более конкретным именем, например,PositionKind
. Его импортируем черезusing static
, и готово. Но это попросту неудобно, каждый раз писать и импортировать.Можно через лямбды:
s.Without("suffix", _ => _.FromEnd)
,s.Without("suffix", _ => _.FromStart)
. Но это тоже не очень удобно и громоздко.s.Without("suffix", from: "end")
— упираемся в возможность ошибиться при вводе"end"
.Вероятно, лучше всего использовать совет от qw1 и взять
s.WithoutSuffix("Builder")
,s.WithoutPrefix("Abstract")
иs.Without("part")
как замены предложенным мной вариантам с цепочками.Да, ещё можно остановиться на
s.RemoveFromStart("suffix")
. Как можно остановиться наDirectory.CreateDirectory
илиQueueUserWorkItem
.Но, на мой взгляд, мы как раз делаем то, что нужно: размышляем на тему того, как подобрать однозначное и читаемое название, используя ограничения того языка, который имеется.
К счастью, традиции и идиомы устаревают. И уже сегодня написано огромное количество fluent-библиотек (хотя не все они Fluent). Скажем,
FluentAssertions
иNSubstitute
.Мне кажется, вполне ожидаемо, что интерфейс на английском языке привязывается к английскому языку.
В
x.Without(".exe")
его нет. Оно есть вx.Without(".exe").AtEnd
. Вот какое:Если дискуссия об этом, то не скажу, что совсем уж плохо, но что есть ряд особенностей — это верно.
Но идея как раз в том, чтобы писать код понятно, отвлечённо от языка. Если вы можете, то почему нет?
Посмотрите, например, на
Spark
. Вот пример API сохранения сразу на нескольких языках:Python
Java
Не будем же мы писать вот такое:
И утверждать, что ребята полезли не в тот язык не с той концепцией? Боюсь, нет.
Сравнение броское, но, как это часто бывает со сравнениями, отвлекает от сути. Чья-то действительность — C# или Java. В которых можно писать красивее, элегантнее, проще и опрятнее, чем
GetUserById
илиUtilsManager
.А как быть, например, с
FluentAssertions
, которые позволяют:Как видите, не имеет значения, что объекты могут быть бесполезными сами по себе. Когда вы знаете грамматику, вам и в голову не придёт коверкать её только потому, что это возможно.
Интересно, как из того факта, что "писать приходится на формальном языке" вырастает
StringTools
? Почему его не нужно всегда уточнять, аAtEnd
нужно? Как отменяется сама форма естественного языка: субъект предложения, объект? Почему из невозможности написать совсем понятно, следует то, что нужно писать совсем непонятно?Так или иначе, на мой взгляд, обсуждение одного примера (когда их было много и с разных сторон) в разрезе целой идеи — это некорректно.
Да, среди множества способов улучшения читаемости, существуют такие, которые поддерживаются языком или недостаточно полно, или совсем частично. Но из этого, как мне кажется, не следует, что:
а) так писать не нужно;
b) нужен другой язык;
c) нужно писать
Tools
иUtils
.Я тоже.
Кстати, верно, этого я не знал, прошу прощения!
Тем не менее, сравните сложность слов
Remove
иSuffix
иWithout
иEnd
(At
мы в расчёт не берём). В первом случае вам нужно привлекать специалиста по computer science, а во втором — не обязательно. При этом мы, разумеется, ещё учитываем почти полную тождественностьWithout(x).AtEnd
тому, как это произносится в речи.Вот то самое стремление к минимальной сложности (т.е. к упрощению) и получается.
Из этого не следует, что она сложнее. Для
LINQ
запросов тоже нужны функции для завершения, и, тем не менее, они не являются сложными.Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано
fileName.Without(".exe").AtEnd
— ребус, а вот если скажет заказчик: "Here I'll need to show file name without .exe at end, can you do that?", то вопросов нет?Кстати, если "show file name without extension", то сразу:
fileName.WithoutExtension()
(что предполагаетAtEnd
).Но
fileName
не меняется после вызоваRemove
. Кроме того, суффикс — это понятие слова, а не целой строки.Писать понятный и читаемый код — это не новый стиль.
Ещё лет двадцать назад Гради Буч в книге "Object Oriented Analysis and Design with Applications" цитировал другую работу 1989 года (Lins, C. 1989. A First Look at Literate Programming. Structured Programming.) — "Software should be written as carefully as English prose, with consideration given to the reader as well as to the computer".
Ему же приписывают: "Clean code reads like well-written prose".
На мой взгляд, разница утрирована. Куда точнее: "Все вокруг пишут философские трактаты, а нужно, оказывается, писать понятно и просто? Зачем?".
Мне кажется, это придуманное, а не фактическое положение вещей.
Что технического в
CopyUtil
?Это и есть копирование.
Так в этом и суть: сущности не самодостаточны, а читаются и используются. Как технари мы боремся против сложности и избыточности, стремимся к простоте и понятности.
Мало написать и реализовать алгоритм — это ничто по сравнению с тем, чтобы написать его доступным для других.
Потерялась папка.
Я бы предложил тогда:
Зачем
CopyUtil
? Что это такое?(Промахнулся веткой.)
Не уверен, что утверждал именно это, предлагая
x.Without(y).AtEnd
.Если смотреть на это с такой точки зрения, то да. Поэтому я и предлагаю совершить ментальный скачок (в статьях описывая его суть): смотреть на код не только как на объекты, методы, вызовы, свойства, а как на выражения и предложения.
С такой точки зрения
"Name_postfix".Without("_postfix").AtEnd
читается ровно так, как вы бы ожидали услышать в естественной речи. Или, скажем:Это может показаться магией, но по тем же причинам магией может показаться управление памятью или даже сама платформа .NET. И тем не менее, мы обобщаем штуки, чтобы писать более красивые и удобные штуки, чтобы потом, обобщая их, писать кое-что ещё лучше.
Но говорим-то мы "copy files".
А вот же.
Это размазало бы статью. Особенно если учесть, что реализовать такое нехитро, речь ведь о форме и именах, а не алгоритмах.
Не уверен. Как правило, так же, как и всё остальное. Что непривычно кому-то, кто всю жизнь писал
GetUserById
, — это верно.Если вы про создание в рантайме, то overhead'а почти всегда нет, ведь создаются структуры. Если про написание кода, то да, определённый overhead появляется ввиду того, что в языке C# нет удобных способов реализовывать однозначные цепочки запросов.
Большая часть материала — про имена и названия (которые определяют содержание), так что на производительность они никак не влияют.
В вопросах производительности контекст первичен. Иначе разработчики платформы .NET не написали бы LINQ, следуя указанным выше критериям.
Иными словами, если красивый код чего-то стоит, пусть стоит, не вызывайте его в
while (true)
.В моём опыте в основном было так: люди считают замыкания и упаковки, не понимая количество вызовов и общий трафик; избавляются от LINQ (пусть и в Common), хотя общая тенденция проектов такая, что 99% кода не влияют на производительность. Всё это излишне. Как правило, и так понятно: "О, вот тут что-то мне надо рекурсивно будет считать, теоретический масштаб вот какой, тогда, пожалуй, проверю, как оно себя ведёт, и подумаю, заменить ли
List
наHashSet
".Из этого не следует, что код должен быть некрасивым, неаккуратным, избыточным и многословным.
Это несущественно, но точнее, если не ошибаюсь, как раз
with
, а неby
. Пожалуйста.Мне так не кажется. Простой и читаемый код важнее, чем внутренняя согласованность библиотеки. Но речь, разумеется, не про Microsoft конкретно, а про общий подход.
Зачем так, если можно хотя бы
Directories.Of(path)
иFiles.Of(path)
? Глагол не нужен совершенно. При этом лучше вообще использовать методы расширений, чтобы передать "субъектный" оттенок:path.AsDirectory().Files
иpath.AsDirectory().Directories
. Тогда и расширяемость выше.Это потому что вы знаете, как устроен
ThreadPool
внутри. А метод, меж тем, не должен сообщать, что он делает внутри.Мне, как клиенту
ThreadPool.QueueUserWorkItem
совершенно безразлично, будет там внутри очередь илиScheduler
. У меня есть действие, и я хочу выполнить его на потоке из пула. Поэтому: "Пул, подыщи-ка мне поток, и выполни эту задачу" — вот вам иFindFreeThreadAndExecuteUserWorkItem
получился.Если прочитать только
Find
, но там есть ещёAndExecute
.В общем, на мой взгляд, всё это несущественные споры. Основная претензия была не
Queue
(хотя и к нему есть), а кUserWorkItem
.Как вам
ThreadPool.Queue
?