На Reddit обсуждают, каких возможностей не хватает C# и .NET. Автор поста поделился своим списком пожеланий, сформированным за годы работы с языком.
Среди предложений:
Автор подчёркивает, что C# ему нравится, но язык мог бы развиваться в сторону большей прозрачности и контроля над производительностью.
Какие функции вы хотели бы видеть в C# и dotnet?🤔
👉 @KodBlog
Среди предложений:
• статическое наследование как способ переиспользования логики без необходимости создавать экземпляры классов
• ключевое слово для гарантированного использования структур без распределения в куче
• принудительное инлайнинг методов, а не лишь намек компилятору
• ручной контроль над GC, похожий на подход Unity, чтобы критичный код не зависел от решений рантайма
• возможность создавать классы, которые полностью живут в unmanaged-памяти
• компактные ключевые слова для базовых типов, которые в .NET выглядят излишне громоздко
Автор подчёркивает, что C# ему нравится, но язык мог бы развиваться в сторону большей прозрачности и контроля над производительностью.
Какие функции вы хотели бы видеть в C# и dotnet?
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19🥴8🤨3
Media is too big
VIEW IN TELEGRAM
Отличная опенсорсная альтернатива Postman
Называется Requestly, и что самое безумное — она полностью open source и доступна как десктоп-приложение и как браузерное расширение.
Качай под свою ОС или устанавливай в браузер и просто начинай тестировать API. Очень просто, аккуратно и без тяжелой настройки😏
Исходники: github.com/requestly/requestly
👉 @KodBlog
Называется Requestly, и что самое безумное — она полностью open source и доступна как десктоп-приложение и как браузерное расширение.
Качай под свою ОС или устанавливай в браузер и просто начинай тестировать API. Очень просто, аккуратно и без тяжелой настройки
Исходники: github.com/requestly/requestly
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤4😁1
Понимание, почему Type.FullName может возвращать null в .NET
Недавно коллега спросил интересную вещь: почему Type.FullName в некоторых случаях возвращает null? Хотя кажется, что метод должен всегда отдавать строку, сигнатура намекает на исключения:
На первый взгляд это может показаться странным, но есть конкретные ситуации, когда рантайм .NET просто не может сформировать валидное полное имя типа. Ниже два основных случая, когда Type.FullName возвращает null.
1. Открытые дженерики
Если создать дженерик с несвязанными (unbound) параметрами:
Здесь тип неполный, его параметры не конкретизированы, поэтому нормальное FullName создать невозможно.
2. Function Pointers
Функциональные указатели, появившиеся в C# 9, тоже отдают null в FullName:
В обоих случаях поведение задумано и соответствует спецификации: если тип не может быть корректно представлен в виде имени, FullName будет null.
👉 @KodBlog
Недавно коллега спросил интересную вещь: почему Type.FullName в некоторых случаях возвращает null? Хотя кажется, что метод должен всегда отдавать строку, сигнатура намекает на исключения:
public abstract string? FullName { get; }На первый взгляд это может показаться странным, но есть конкретные ситуации, когда рантайм .NET просто не может сформировать валидное полное имя типа. Ниже два основных случая, когда Type.FullName возвращает null.
1. Открытые дженерики
Если создать дженерик с несвязанными (unbound) параметрами:
var list = typeof(IList<>);
var dict = typeof(IDictionary<,>);
var listOfDictionaries = list.MakeGenericType(dict); // IList<IDictionary<,>>
Assert.Null(listOfDictionaries.FullName);
Здесь тип неполный, его параметры не конкретизированы, поэтому нормальное FullName создать невозможно.
2. Function Pointers
Функциональные указатели, появившиеся в C# 9, тоже отдают null в FullName:
var functionPointerType = typeof(delegate*<int, void>);
Assert.Null(functionPointerType.FullName);
В обоих случаях поведение задумано и соответствует спецификации: если тип не может быть корректно представлен в виде имени, FullName будет null.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤4🔥4😁1
Есть разработчик, который провалил собеседование, потому что не знал, как работают блокировки в C#.
Задача была вроде обычная: API конвертации валют + кэш. Но при большом количестве параллельных запросов возникал риск cache stampede. Он попытался решить это через lock, но метод был async. А lock с async-await не работает. Логично: никто не гарантирует, какой поток завершит выполнение.
Правильный вариант в таких случаях — асинхронные примитивы синхронизации вроде SemaphoreSlim, Semaphore, Mutex или Monitor.
Два базовых правила при работе с таким кодом:
1. Использовать timeout
2. Освобождать блокировку в try-finally
И напоследок хороший вопрос:
🚬
Кому интересно — ответ
👉 @KodBlog
Задача была вроде обычная: API конвертации валют + кэш. Но при большом количестве параллельных запросов возникал риск cache stampede. Он попытался решить это через lock, но метод был async. А lock с async-await не работает. Логично: никто не гарантирует, какой поток завершит выполнение.
Правильный вариант в таких случаях — асинхронные примитивы синхронизации вроде SemaphoreSlim, Semaphore, Mutex или Monitor.
Два базовых правила при работе с таким кодом:
1. Использовать timeout
2. Освобождать блокировку в try-finally
И напоследок хороший вопрос:
как реализовать блокировку на уровне базы данных?
Кому интересно — ответ
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍8🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
VS Code выкатывает новую фичу в терминале, и это просто имба.
Если нужно выполнить команду, но ты не помнишь синтаксис или точный аргумент, больше не надо гуглить или вспоминать по памяти. Достаточно нажать
Дальше всё просто: пишешь нормальным человеческим языком, что хочешь сделать. VS Code сам определит нужную команду и выполнит её.
👉 @KodBlog
Если нужно выполнить команду, но ты не помнишь синтаксис или точный аргумент, больше не надо гуглить или вспоминать по памяти. Достаточно нажать
Ctrl+i прямо в терминале. Вылезет компактное окно с чатом.Дальше всё просто: пишешь нормальным человеческим языком, что хочешь сделать. VS Code сам определит нужную команду и выполнит её.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯20❤8🔥4❤🔥1👍1
Используем insteadOf в Git для Замены HTTPS-адресов на SSH
При работе с Git-репозиториями вы часто сталкиваетесь с URL-адресами в формате HTTPS, особенно при клонировании из GitHub, GitLab или других хостинг-провайдеров. Однако, если вы предпочитаете использовать SSH для аутентификации (что часто удобнее с аутентификацией по ключам), ручное изменение URL-адресов может быть утомительным. Кроме того, это может быть ещё сложнее при работе с субмодулями.
Конфигурационная опция Git insteadOf предлагает элегантное решение, автоматически переписывая URL-адреса «на лету»:
Вы также можете настроить более конкретную проверку и выбрать только определённые репозитории или сервисы. Например, если вы хотите заменить HTTPS-адреса некоторых репозиториев, вы можете сделать следующее:
После настройки Git будет автоматически переписывать URL:
Перезапись происходит прозрачно. Вы по-прежнему можете использовать HTTPS-адреса в командах, документации или при копировании из веб-интерфейсов.
Ещё один вариант использования insteadOf — добавление аутентификации к URL. Например, если вы хотите использовать определённого пользователя для всех запросов Git, вы можете сделать следующее:
👉 @KodBlog
При работе с Git-репозиториями вы часто сталкиваетесь с URL-адресами в формате HTTPS, особенно при клонировании из GitHub, GitLab или других хостинг-провайдеров. Однако, если вы предпочитаете использовать SSH для аутентификации (что часто удобнее с аутентификацией по ключам), ручное изменение URL-адресов может быть утомительным. Кроме того, это может быть ещё сложнее при работе с субмодулями.
Конфигурационная опция Git insteadOf предлагает элегантное решение, автоматически переписывая URL-адреса «на лету»:
git config --global url."[email protected]:".insteadOf "https://github.com/"
git config --global url."[email protected]:".insteadOf "https://gitlab.com/"
git config --global url."[email protected]:".insteadOf "https://bitbucket.org/"
Вы также можете настроить более конкретную проверку и выбрать только определённые репозитории или сервисы. Например, если вы хотите заменить HTTPS-адреса некоторых репозиториев, вы можете сделать следующее:
git config --global url."[email protected]:username/".insteadOf "https://github.com/username/"
После настройки Git будет автоматически переписывать URL:
# Следующая команда
git clone https://github.com/user/repo.git
# Станет эквивалентной
git clone [email protected]:user/repo.git
Перезапись происходит прозрачно. Вы по-прежнему можете использовать HTTPS-адреса в командах, документации или при копировании из веб-интерфейсов.
Ещё один вариант использования insteadOf — добавление аутентификации к URL. Например, если вы хотите использовать определённого пользователя для всех запросов Git, вы можете сделать следующее:
git config --global url."https://<token>@github.com/".insteadOf "https://github.com/"
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5
Удаление пробелов в .NET: что быстрее
На Хабре вышло сравнение разных способов убрать пробелы из строки в .NET: Replace, Regex, Split+Concat, StringBuilder, буферы и stackalloc. Тестировали на .NET 3.1, .NET 8 и .NET 10.
Результаты:
Автор напоминает, результат зависит от реальных данных, так что лучше тестировать под свой кейс.
Подробнее тут: habr.com/ru/companies/skbkontur/articles/970178/
👉 @KodBlog
На Хабре вышло сравнение разных способов убрать пробелы из строки в .NET: Replace, Regex, Split+Concat, StringBuilder, буферы и stackalloc. Тестировали на .NET 3.1, .NET 8 и .NET 10.
Результаты:
- stackalloc и буферный подход оказались самыми быстрыми
- StringBuilder почти не отстаёт
- Split+Concat работает шустро, но жрет память
- у Regex в .NET 10 заметно выросла производительность
Автор напоминает, результат зависит от реальных данных, так что лучше тестировать под свой кейс.
Подробнее тут: habr.com/ru/companies/skbkontur/articles/970178/
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍8😁4🔥3
Один разработчик поделился экспериментом: он пересмотрел видео tsoding про работу с PPM-форматом и повторил всё на C#. При этом отметил, что реализация на C выглядит куда компактнее, но сама идея остаётся простой и понятной.
PPM выбран ради простоты. Формат состоит из текстового заголовка с описанием параметров изображения, а дальше идут байты по три штуки на пиксель, которые задают RGB.
Для динамики он сгенерировал 60 изображений, а затем собрал их в видео через ffmpeg. На каждом шаге узор немного смещался, и в результате получилась анимация движущейся шахматной сетки.
Классная мини-задача. Очень советую.
👉 @KodBlog
PPM выбран ради простоты. Формат состоит из текстового заголовка с описанием параметров изображения, а дальше идут байты по три штуки на пиксель, которые задают RGB.
Для динамики он сгенерировал 60 изображений, а затем собрал их в видео через ffmpeg. На каждом шаге узор немного смещался, и в результате получилась анимация движущейся шахматной сетки.
Классная мини-задача. Очень советую.
Производительность не была для меня целью, поэтому я не обращал на это внимания.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4😁2😐2
Как сделать API-эндпоинты быстрее в 426 раз
(подсказка: дело не в кешe)
Когда разработчики видят медленный API, первая реакция: лечить всё кешем.
Но это ошибка в базовом понимании:
кеш не фиксит тормоза, если корень проблемы в кривых запросах к базе.
Поэтому первый шаг к масштабируемой системе — пофиксить её, а не обклеить кешами.
Небольшой эксперимент:
есть веб-API с одним эндпоинтом
До оптимизации:
Обработано запросов: 378
Средний RPS: 11.01
Средняя длительность запроса: ~4 секунды
После оптимизации:
Обработано запросов: 140 331
Средний RPS: 4 689.36
Средняя длительность запроса: 10.69 мс
Сначала фикси доступ к данным.
Потом уже используй кеш, чтобы масштабироваться, а не выживать.
👉 @KodBlog
(подсказка: дело не в кешe)
Когда разработчики видят медленный API, первая реакция: лечить всё кешем.
Но это ошибка в базовом понимании:
кеш не фиксит тормоза, если корень проблемы в кривых запросах к базе.
Поэтому первый шаг к масштабируемой системе — пофиксить её, а не обклеить кешами.
Небольшой эксперимент:
есть веб-API с одним эндпоинтом
/productsДо оптимизации:
Обработано запросов: 378
Средний RPS: 11.01
Средняя длительность запроса: ~4 секунды
После оптимизации:
Обработано запросов: 140 331
Средний RPS: 4 689.36
Средняя длительность запроса: 10.69 мс
Сначала фикси доступ к данным.
Потом уже используй кеш, чтобы масштабироваться, а не выживать.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10🤣5🔥1🥴1
Представили: GitList для linqpad
В LINQPad мне давно хотелось управлять Git-изменениями прямо внутри приложения.
Теперь это реально: Git-репы открываются как кастомное подключение, и ими можно управлять напрямую в LINQPad. Под капотом используется LibGit2Sharp, чтобы читать состояние репозитория и показывать данные прямо в интерфейсе.
Статус: альфа
Что нужно: тестирование
NuGet: LearningLINQPad.GitList
GitHub: https://github.com/ryanrodemoyer/LearningLINQPad-GitList
Примечание: будьте максимально осторожны с важными данными. Инструменту нужно ещё много тестов!
👉 @KodBlog
В LINQPad мне давно хотелось управлять Git-изменениями прямо внутри приложения.
Теперь это реально: Git-репы открываются как кастомное подключение, и ими можно управлять напрямую в LINQPad. Под капотом используется LibGit2Sharp, чтобы читать состояние репозитория и показывать данные прямо в интерфейсе.
Статус: альфа
Что нужно: тестирование
NuGet: LearningLINQPad.GitList
GitHub: https://github.com/ryanrodemoyer/LearningLINQPad-GitList
Примечание: будьте максимально осторожны с важными данными. Инструменту нужно ещё много тестов!
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍2
Использование выражений коллекций для своих типов
В C# 12 появились выражения коллекций — упрощённый синтаксис для инициализации коллекций:
Такой синтаксис отлично работает со стандартными коллекциями, но что делать с собственными типами? Здесь пригодится атрибут CollectionBuilderAttribute, который позволяет подключить этот новый синтаксис к пользовательским коллекциям.
Пользовательские коллекции
Допустим, у вас есть свой тип коллекции:
Раньше вы не могли использовать выражения коллекций с таким типом. Приходилось писать по-старинке:
Или так (что сути почти не меняет):
Не особо изящно.
Атрибут CollectionBuilderAttribute решает проблему, подсказывая компилятору, как собирать вашу коллекцию из выражения коллекции:
Компилятор сам вызовет метод Create и передаст ему элементы.
Как это работает
Атрибут принимает два параметра:
Тип построителя — класс, в котором находится фабричный метод (typeof(MyCollectionBuilder));
Имя метода — имя статического метода, создающего объект коллекции ("Create").
Метод построителя должен:
- быть статическим;
- принимать ReadOnlySpan<T> (предпочтительно) или T[];
- возвращать экземпляр вашей коллекции;
- иметь параметры типов, совпадающие с параметрами коллекции.
Замечание: коллекция должна иметь «тип итерации», то есть метод GetEnumerator(), который возвращает IEnumerator или IEnumerator<T>. Можно реализовать IEnumerable / IEnumerable<T>, либо просто добавить метод GetEnumerator() вручную.
Когда все условия выполнены, компилятор сможет генерировать нужный код и создавать вашу коллекцию максимально эффективно.
👉 @KodBlog
В C# 12 появились выражения коллекций — упрощённый синтаксис для инициализации коллекций:
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Такой синтаксис отлично работает со стандартными коллекциями, но что делать с собственными типами? Здесь пригодится атрибут CollectionBuilderAttribute, который позволяет подключить этот новый синтаксис к пользовательским коллекциям.
Пользовательские коллекции
Допустим, у вас есть свой тип коллекции:
public class MyCollection<T>
{
private readonly List<T> _items;
public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];
public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();
// другие члены…
}
Раньше вы не могли использовать выражения коллекций с таким типом. Приходилось писать по-старинке:
var myCol =
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });
Или так (что сути почти не меняет):
var myCol =
new MyCollection<int>([1, 2, 3, 4, 5]);
Не особо изящно.
Атрибут CollectionBuilderAttribute решает проблему, подсказывая компилятору, как собирать вашу коллекцию из выражения коллекции:
[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
//…
}
public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}
Теперь можно писать так:
MyCollection<int> myCol = [1, 2, 3, 4, 5];
Компилятор сам вызовет метод Create и передаст ему элементы.
Как это работает
Атрибут принимает два параметра:
Тип построителя — класс, в котором находится фабричный метод (typeof(MyCollectionBuilder));
Имя метода — имя статического метода, создающего объект коллекции ("Create").
Метод построителя должен:
- быть статическим;
- принимать ReadOnlySpan<T> (предпочтительно) или T[];
- возвращать экземпляр вашей коллекции;
- иметь параметры типов, совпадающие с параметрами коллекции.
Замечание: коллекция должна иметь «тип итерации», то есть метод GetEnumerator(), который возвращает IEnumerator или IEnumerator<T>. Можно реализовать IEnumerable / IEnumerable<T>, либо просто добавить метод GetEnumerator() вручную.
Когда все условия выполнены, компилятор сможет генерировать нужный код и создавать вашу коллекцию максимально эффективно.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍6👎1
Вот моё любимое правило в .editorconfig:
-> Правила именования для async-методов и полей
Задайте понятные правила именования для приватных полей, async-методов, интерфейсов и так далее.
Зачем? это убирает путаницу при работе с async-API и делает намерения очевидными.
Если кто не в курсе:
.editorconfig — это обычный текстовый конфиг, который сообщает вашему редактору или IDE, как форматировать и оформлять код, чтобы весь проект выглядел одинаково, независимо от личных настроек каждого разработчика.
Он работает во многих языках и IDE (Visual Studio, VS Code, JetBrains Rider и прочих), а в .NET особенно полезен, потому что встроен в Roslyn-анализаторы и настройки стиля кода.
👉 @KodBlog
-> Правила именования для async-методов и полей
Задайте понятные правила именования для приватных полей, async-методов, интерфейсов и так далее.
Зачем? это убирает путаницу при работе с async-API и делает намерения очевидными.
Если кто не в курсе:
.editorconfig — это обычный текстовый конфиг, который сообщает вашему редактору или IDE, как форматировать и оформлять код, чтобы весь проект выглядел одинаково, независимо от личных настроек каждого разработчика.
Он работает во многих языках и IDE (Visual Studio, VS Code, JetBrains Rider и прочих), а в .NET особенно полезен, потому что встроен в Roslyn-анализаторы и настройки стиля кода.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍6🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Это расширение для Chrome очень полезно для тех, кто сидит на GitHub.
Называется SimRepo. Оно показывает похожие репозитории для любого репо, который ты открываешь.
Просто устанавливаешь расширение, и оно автоматически срабатывает каждый раз, когда ты заходишь в любой репозиторий.
👉 @KodBlog
Называется SimRepo. Оно показывает похожие репозитории для любого репо, который ты открываешь.
Просто устанавливаешь расширение, и оно автоматически срабатывает каждый раз, когда ты заходишь в любой репозиторий.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤3
Сжатие запросов HttpClient с помощью GZIP
Если отправляешь большие файлы во внешний API, логично сжимать их перед отправкой. Но наивная реализация легко убивает память.
Проблемный вариант
Сжатие происходит полностью в MemoryStream, а потом уже уходит в запрос:
Использование:
Работает нормально, пока не начинаешь слать много запросов.
Вся gzip-копия каждый раз полностью живет в памяти.
Правильное решение: сжатие на лету
Переделываем CreateRequest:
Использование остается тем же, а память больше не пухнет.
Производительность (.NET 10)
По времени почти без разницы,
а вот по памяти разница огромная:
Чем больше файл, тем сильнее эффект.
PS: .NET API может автоматически распаковывать запросы. Достаточно добавить соответствующее промежуточное ПО:
👉 @KodBlog
Если отправляешь большие файлы во внешний API, логично сжимать их перед отправкой. Но наивная реализация легко убивает память.
Проблемный вариант
Сжатие происходит полностью в MemoryStream, а потом уже уходит в запрос:
internal async ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
var outStream = new MemoryStream();
await using (var zip =
new GZipStream(outStream,
CompressionMode.Compress,
leaveOpen: true))
{
await source.CopyToAsync(zip);
zip.Close();
}
outStream.Position = 0;
var request = new HttpRequestMessage(
HttpMethod.Post, uri)
{
Content = new StreamContent(outStream)
};
request.Content.Headers.ContentEncoding.Add("gzip");
return request;
}
Использование:
var uri = new Uri("https://my.api/endpoint");
var file = File.OpenRead("largefile.txt");
var request = await CreateRequest(uri, file);
await httpClient.SendAsync(request);Работает нормально, пока не начинаешь слать много запросов.
Вся gzip-копия каждый раз полностью живет в памяти.
Правильное решение: сжатие на лету
public class GzipContent : HttpContent
{
private readonly Stream _source;
public GzipContent(Stream source)
{
_source = source;
Headers.ContentEncoding.Add("gzip");
}
protected override async Task
SerializeToStreamAsync(
Stream stream,
TransportContext? context)
{
await using var zip =
new GZipStream(stream,
CompressionMode.Compress,
leaveOpen: true);
await _source.CopyToAsync(zip);
}
protected override bool
TryComputeLength(out long length)
{
length = -1;
return false;
}
}
Переделываем CreateRequest:
internal ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
return ValueTask.FromResult(
new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new GzipContent(source)
});
}
Использование остается тем же, а память больше не пухнет.
Производительность (.NET 10)
По времени почти без разницы,
а вот по памяти разница огромная:
| File | Method| Mean | Ratio| Allocated| Alc Rat |
|-----:|-------|-------:|-----:|---------:|--------:|
| 7KB| Memory| 633.3us| 1.00| 6.35KB| 1.00|
| 7KB| Gzip | 603.5us| 0.96| 4.77KB| 0.75|
| 200KB| Memory| 8.025ms| 1.00| 508.52KB| 1.000|
| 200KB| Gzip | 7.260ms| 0.90| 4.74KB| 0.009|
| 3MB| Memory| 94.97ms| 1.00| 8189.99KB| 1.000|
| 3MB| Gzip | 86.02ms| 0.91| 4.82KB| 0.001|
Чем больше файл, тем сильнее эффект.
PS: .NET API может автоматически распаковывать запросы. Достаточно добавить соответствующее промежуточное ПО:
builder.Services.AddRequestDecompression();
// …
app.UseRequestDecompression();
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👌5👍2
GitHub теперь в Telegram!
Самый прогерский канал, где за 10 минут ты научишься:
/ Пробив по фото и номеру в ТГ
// Как взломать вебку подруги
/// Мануал по OSINT разведке
Подписывайся, нас уже сотни тысяч: >@GitHub
Самый прогерский канал, где за 10 минут ты научишься:
/ Пробив по фото и номеру в ТГ
// Как взломать вебку подруги
/// Мануал по OSINT разведке
Подписывайся, нас уже сотни тысяч: >@GitHub
🥴4🔥2👎1🤨1
Синтаксическое сжатие строк даёт больше вертикального пространства в редакторе. Строки без букв и цифр сжимаются на 25%, благодаря чему на экране помещается больше кода без потери читаемости.
Скоро в Visual Studio.
👉 @KodBlog
Скоро в Visual Studio.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🥰6❤1
