C# — есть ли что-то лишнее?

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2016-06-19 10:35

лингвистика

Все будет быстро. Это выступление Анатолия Левенчука, в последнее время не дает мне покоя. Успехи глубинного обучения в последний год говорят о том, что все очень быстро изменится. Слишком долго кричали волки-волки говорили «искусственный интеллект», а его все не было. И вот, когда он, наконец, приходит к нам, многие люди этого просто не осознают, думая, что все закончится очередной победой компьютера в очередной интеллектуальной игре. Очень многие люди, если не все человечество, окажется за бортом прогресса. И этот процесс уже запущен. Думаю, что в этот момент меня не очень будет интересовать вопрос, который вынесен в заголовок статьи. Но, пока этот момент еще не настал, попытаюсь поднять этот потенциально спорный вопрос.

Программируя уже более 25 лет, застал достаточно много различных концепций, что-то смог попробовать, еще больше не успел. Сейчас с интересом наблюдаю за языком Go, который можно отнести к продолжателям -линейки языков Вирта- - Algol-Pascal-Modula-Oberon. Одним из замечательных свойств этой цепочки является то, что каждый последующий язык становится проще предыдущего, но не менее мощным и выразительным.

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

  • Простой язык быстрее изучается, значит проще получить необходимых разработчиков.
  • Поддержка программы на более простом языке обычно проще (да, это интуитивное ощущение, которое нужно бы доказать, но я приму его сейчас за аксиому).
  • У более простого языка проще развивать окружающую его инфраструктуру - переносить на разные платформы, создавать различные утилиты, генераторы, парсеры и т.п.

Почему же тогда существуют сложные языки? Все дело в выразительности. Если какая-то конструкция позволяет коротко описать необходимое действие, то это вполне может окупить негативные стороны усложнения языка.

За относительно недолгое время своего существования, язык C# впитал в себя значительное количество различных концепций, отразившихся в его конструкциях. Скорость их добавления иногда пугает. Мне, поскольку я с C# почти с самого начала - проще. Но каково новичкам, которые только приступают к изучению? Иногда завидую Java-программистам, где новшества внедряются в язык гораздо более консервативно.

То, что добавлено в язык - его ведь реально уже не вырубишь и топором. Конечно, если взять язык, широко распространенный в узких кругах, можно позволить себе несовместимость между версиями. Некоторые -шалости- обратной несовместимости может себе позволить такой язык, как Python (при переходе со 2-й на 3-ю версию). Но не C#, за которым стоит Майкрософт. Мне кажется, что если бы разработчики понимали, что с каждой новой фичей язык становится не только удобнее (в определенных случаях), но и немного ближе к своей смерти от -ожирения-, то комментарии были бы чуть менее восторженными, чем это имеет место в первой ветке откликов на новшества C# 7.

То, что описано далее - всего лишь мои спекуляции на тему того, действительно ли это полезная штука. Конечно, это может быть делом вкуса и не все согласятся со мной (смотрите спойлеры). И в любом случае, это останется в C# уже навечно- Ну, до момента сингулярности, по крайней мере.

Список добавленных фич языка по версиям можно найти здесь: C# Features added in versions. Не буду трогать версию 2.0, начну с 3.0.

Лирические воспоминания

C# 3.0

Implicitly typed local variables

var i = 5; var s = "Hello"; var d = 1.0; var numbers = new int[] {1, 2, 3}; var orders = new Dictionary<int,Order>();

Пресловутое var. О введении которого сейчас спорят в мире Java («Var и val в Java?», «Ключевое слово «var» в Java: пожалуйста, только не это»)

Код пишется один раз, читается много (банальная истина). Автоматический вывод типа во многих случаях заставляет делать дополнительные действия для того, чтобы понять, какого типа переменная. А значит, это плохо. Да, это привычно, например, для JavaScript-программистов, но там совершенно другая парадигма типизации.

Раздражение от явного и полного прописывания типов вызывают такие вот куски кода:

List<Pair<String, Double>> scores = seeker.getScores(documentAsCentroid); ... foreach(Pair<String, Double> score in scores)

И это (Pair<String, Double>) далеко не самый длинное определение типа, которое приходится повторять. А любые повторы - это действительно плохо (помимо того, что просто неуклюже). Но есть способ значительно лучше и выразительнее. Вот чего мне после Паскаля не хватало в Java, а затем в C#, так это конструкции типа Type (typedef в C). В C# под это дело пытался приспособить using, который позволяет в начале файла написать что-то типа:

using StopWordsTables = System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, string>>;

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

Вот если бы ввели typedef, это бы решило проблему громоздких типов без ущерба для читаемости кода.

Возражения по поводу того, что можно было бы договориться использовать var только там, где тип легко вывести глазами (т.е., он явно виден в инициализаторе) не найдут у меня поддержки по одной простой причине. Аналогичное правило уже ввела в своем Code Agreement Майкрософт (про свою компанию я уже молчу). Только вот практически никто этого не соблюдает. Var победило. Люди ленивы.

Есть еще момент - var очень ограничен. Его можно использовать только в локальных идентификаторах. В свойствах, полях, методах, все также приходится раз за разом писать эти раздражающе длинные идентификаторы коллекций, а в случае изменения типов повторять редактирование во всех местах. С Type/typedef этого все ушло бы в прошлое.

В развитие темы - если уж ввели var, почему бы не довести идею уже до логического завершения, как это сделано в Go? В инициализаторе вместо -=- писать -:=-, что означает, что тип выводится автоматически. И тогда вообще не нужно никакого слова писать на месте типа. Еще короче- Кстати, type в Go тоже есть, что очень удобно.

Мой вывод - var в C# был ошибкой. Нужен был всего лишь typedef.

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

Object and collection initializers

var r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } }; List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Штука полезная, сокращающая код не в ущерб читаемости. Одобряю.

Auto-Implemented properties

Теперь, вместо

public Class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }

Стало возможно писать так:

public Class Point { public int X { get; set; } public int Y { get; set; } }

Насчет свойств в глубине души так и не понял, а нужно ли их было вводить? Вон, в той же Java и без них вполне нормально жить, используя определенные соглашения имен в методах. Но коль уж ввели, то такое упрощение их описания вполне удобно (без ухудшения читабельности) во многих случаях.

Anonymous types

var p1 = new { Name = "Lawnmower", Price = 495.00 }; var p2 = new { Name = "Shovel", Price = 26.95 }; p1 = p2;

Мне данная опция языка так ни разу и не пригодилась. Хотя нет, 1 раз таки нужно было, вспомнил. Я бы не вводил. Хотя те примеры, что видел в учебнике, вроде бы и логичны. В общем, возможно штука и полезная, просто не в моих сценариях (предпочитаю возиться с алгоритмами, а не с базами и JSON, хотя, разное бывает).

Extension methods

namespace Acme.Utilities { public static class Extensions { public static int ToInt32(this string s) { return Int32.Parse(s); } public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) throw new ArgumentException(); T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } } }
using Acme.Utilities; ... string s = "1234"; int i = s.ToInt32(); // Same as Extensions.ToInt32(s) int[] digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int[] a = digits.Slice(4, 3); // Same as Extensions.Slice(digits, 4, 3)

Очень удобная штука. Временами теоретики ООП её ругают, но без неё было бы неудобно (громоздко) делать многие вещи.

Query expressions

Он же LINQ. Этот пункт вызывает настолько смешанные чувства! Ну, примерно, как ложка дегтя в бочке чего-то хорошего. Вне всякого сомнения, LINQ явилась одной самых, по настоящему классных возможностей языка. Но зачем нужно было реализовывать это двумя способами? Я про так называемый человеко-понятный синтаксис (ЧПС), который имитировал SQL-запросы, насколько я понимаю.

string[] people = new [] { "Tom", "Dick", "Harry" }; // ЧПС или же синтаксис запросов var filteredPeople = from p in people where p.Length > 3 select p; // функциональный стиль или лямбда синтаксис var filteredPeople = people.Where (p => p.Length > 3);

В результате имеем:

  • ЧПС не соответствует SQL напрямую, так что знания SQL недостаточно для того, чтобы написать соответствующий запрос. Есть свои особенности.
  • Одно и то же (с небольшими и редкими исключениями, функциональный и ЧПС-стили эквивалентны) можно написать двумя способами.
  • Программисту следует учить оба варианта, поскольку они оба могут появиться в коде.
  • ЧПС стиль резко контрастирует с остальным кодом, выглядя чем-то чужеродным.

Похожие чувства в плане чужеродности стиля у меня вызывают байндинги WPF. Они представляют собой микроскриптовые конструкции, написанные на своем языке внутри XML. В результате все выглядит громоздко и некрасиво. Не знаю, как можно было бы сделать красивее - может создать специализированный язык разметки, а не городить все в XML? Но я отвлекся. В общем, признаюсь - за последние несколько лет не написал ни одного выражения в ЧПС, при этом совершенно не испытывая в этом потребности. Только немножко редактировал чужие.
В общем, LINQ - очень и очень нужная штука, к которой очень зря привесили гирю ЧПС.

Lambda expressions

x => x + 1 // Implicitly typed, expression body x => { return x + 1; } // Implicitly typed, statement body (int x) => x + 1 // Explicitly typed, expression body (int x) => { return x + 1; } // Explicitly typed, statement body (x, y) => x * y // Multiple parameters () => Console.WriteLine() // No parameters

Это было прекрасное приобретение, привнесшее в C# элементы функционального стиля и существенно улучшившего выразительность коротких фрагментов кода, передаваемых как аргументы. Вот только когда лямбды начинают занимать с десяток и более строк, читать код становится очень сложно. Важно вовремя остановиться и в этом случае перейти опять на методы.

Expression trees

Вряд ли стоит рассматривать данную фичу отдельно от LINQ и Lambda.

Partial methods

Неплохой способ разделить автоматически генерируемый и ручной код. Я - за.

Веня, он же Бэн

C# 4.0

Dynamic binding

Потенциально полезный пример - вместо

var doc = new XmlDocument("/path/to/file.xml"); var baz = doc.GetElement("foo").GetElement("qux");

можно написать

dynamic doc = new XmlDocument("/path/to/file.xml"); var baz = doc.foo.qux;

Несмотря на то, что выглядит хорошо, я бы не рекомендовал такое использование. Тип dynamic - очень опасная штука, поскольку теряется весь контроль типов. Из более мелких пакостей - перестают работать подсказки в редакторе. Тем не менее, в определенных сценариях, он полезен. Например, с его помощью я у себя делал подгрузку плагинов (точнее, использование кода из них). За счет того, что вызовы методов здесь кешируются, то получается производительно и не нужно городить это самостоятельно. А насчет безопасности - иначе мне бы все-равно пришлось бы работать через рефлексию, так что в этом случае безопасность не была бы большей. А вот код был бы более сложным и запутанным. Так что осторожное использование динамиков в ограниченном количестве сценариев одобряю. Конечно, вводились они больше с прицелом на скриптовые языки. Ну, нужно, так нужно.

Named and optional arguments

class Car { public void Accelerate( double speed, int? gear = null, bool inReverse = false) { /* ... */ } }
Car myCar = new Car(); myCar.Accelerate(55);

Уменьшается количество перегруженных методов - код становится проще и надежнее (меньше возможностей совершить ошибку копипаста, меньше работы при рефакторинге). Одобряю.

Generic co- and contravariance

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

Embedded interop types («NoPIA»)

Это одна из фич, про которые мне особо нечего сказать, исходя из своей практики - просто читал, что такое есть. Мне она не нужна была, но COM видимо еще долго не умрет и тем, кто (например) работает с MS Office, он еще долго будет нужен.

C# 5.0

Asynchronous methods

public async Task ReadFirstBytesAsync(string filePath1, string filePath2) { using (FileStream fs1 = new FileStream(filePath1, FileMode.Open)) using (FileStream fs2 = new FileStream(filePath2, FileMode.Open)) { await fs1.ReadAsync(new byte[1], 0, 1); // 1 await fs2.ReadAsync(new byte[1], 0, 1); // 2 } }

Очень удобная конструкция. К сожалению, при практической реализации возникли некоторые ограничения - детали реализации протекали в виде ограничений (Async/await в C#: подводные камни). Часть их была снята в следующих версиях (Await in catch/finally blocks) языка, или библиотек (akka.net поначалу не позволяла смешивать свою модель асинхронного исполнения с рассматриваемой фичей, но потом это поправили). Может быть имело бы смысл рассмотреть и какие-то другие паттерны параллельного взаимодействия - типа горутин. Но тут уже выбор за архитекторами языка. В общем, одобряю.

Caller info attributes

public void DoProcessing() { TraceMessage("Something happened."); } public void TraceMessage(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { System.Diagnostics.Trace.WriteLine("message: " + message); System.Diagnostics.Trace.WriteLine("member name: " + memberName); System.Diagnostics.Trace.WriteLine("source file path: " + sourceFilePath); System.Diagnostics.Trace.WriteLine("source line number: " + sourceLineNumber); } // Sample Output: // message: Something happened. // member name: DoProcessing // source file path: c:UsersusernameDocumentsVisual Studio 2012ProjectsCallerInfoCSCallerInfoCSForm1.cs // source line number: 31

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

C# 6.0

Compiler-as-a-service (Roslyn)

Этот пункт (несмотря на общую важность) пропущу. Я бы отнес его скорее к инфраструктуре, а не непосредственно к языку.

Import of static type members into namespace

using static System.Console; using static System.Math; using static System.DayOfWeek; class Program { static void Main() { WriteLine(Sqrt(3*3 + 4*4)); WriteLine(Friday - Monday); } }

Поначалу мне эта фича показалась полезной. Но попробовав её на практике, вынужден признать, что ошибся. Читаемость кода ухудшается - методы и члены статического класса начинают мешаться с методами текущего класса. И даже ввод стал медленнее, хотя вроде бы количество идентификаторов уменьшилось на единицу. Но за счет того, что теперь в подсказке от Intellisense больше вариантов, нажатий нужно сделать больше. В общем, данная фича, с моей точки зрения - ошибка.

Exception filters

try { - } catch (MyException e) when (myfilter(e)) { - }

Еще не попробовал. Поэтому есть искушение назвать фичу бесполезной, но может просто мои сценарии к ней не сильно подходят? Может, кто расскажет, в каких случаях и насколько часто она реально хороша?

Await in catch/finally blocks

Не считаю это самостоятельной фичей - скорее исправление предыдущих проблем.

Auto property initializers

public class Customer { public string First { get; set; } = "Jane"; public string Last { get; set; } = "Doe"; }

Логичное и удобное дополнение к автосвойствам. Код становится чище, а значит одобряю.

Default values for getter-only properties

public class Customer { public string First { get; } = "Jane"; public string Last { get; } = "Doe"; }

Аналогично предыдущему пункту.

Expression-bodied members

public string Name => First + " " + Last; public Customer this[long id] => store.LookupCustomer(id);

Большой практики применения пока нет, но выглядит неплохо. Нужно будет еще пройтись по своему коду и посмотреть, где можно бы применить. Главное здесь как с лямбдами - не переусердствовать и не делать выражений на полэкрана.

Null propagator (succinct null checking)

public static string Truncate(string value, int length) { return value?.Substring(0, Math.Min(value.Length, length)); }

Давно напрашивавшаяся штука. Одобряю. Хотя, на практике и оказалось, что применяется не так часто, как ожидалось до того.

String Interpolation

О! Вот это то, чего ждал давным-давно, и что мгновенно прижилось в моем коде. Всегда старался писать идентификаторы в контексте строки примерно так:

-Total lines: - + totalLines + -, total words: - + totalWords + -.-;

Иногда меня спрашивали, а знаю ли я про форматирование строк? Да, знаю, но там есть 2 большие проблемы:

  • Выражение отнесено далеко от места, где оно вставляется. Это затрудняет чтение кода.
  • Строка с литералами форматирования, фактически является микроскриптом, который исполняется в run-time. Соответственно, вся система типизации, проверки соответствия параметров C# летит в тартарары.

Также это приводит к тому, что в методах Format(...) допускается большое количество ошибок при рефакторинге.
Поэтому и использовал такое вот немного громоздкое написание. Но, наконец, дождался от C# такого подарка :) Одобряю однозначно и всеми конечностями!

nameof operator

if (x == null) throw new ArgumentNullException(nameof(x)); WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"

Аналогично -Caller info attributes-. Одобряю.

Dictionary initializer

var numbers = new Dictionary<int, string> { [7] = "seven", [9] = "nine", [13] = "thirteen" };

Еще одно новшество, которое позволяет упростить работу с кодом, который нередко набирается с помощью копипаста и подвержен ошибкам от невнимательности. Любая возможность сделать его чище и удобнее для чтения будет приводить к плюсам в карме архитектора языка. Плюсик.

Анти-Бэн

C# 7.0 proposals

Данными функциями я еще не пользовался - обычно сижу на релизной версии шарпа, иногда приходится спускаться чуть ниже. Поэтому здесь приведу чисто умозрительные аргументы. Список приведу по статье -Новшества C# 7-, а не по данным из википедии.

Binary literals

int x = 0b1010000; int SeventyFive = 0B10_01011;

Новшество выглядит безобидно, не сильно усложняя язык, а для тех, кому нужно работать с битами - удобно. Немного не понял фразу «Можно отделять нули произвольным количеством подчёркиваний» - почему только нули?

Local functions

public void Foo(int z) { void Init() { Boo = z; } Init(); }

Когда только перешел на C# с Объектного Паскаля (Delphi), мне очень не хватало локальных функций, как способа структурировать свой код. Простое вынесение кусков кода в приватные методы приводило к появлению классов с большим количеством методов на одном уровне. Так происходило, пока я не понял, что в C# для этого нужно применять другой метод - объектную декомпозицию. После этого я часто стал выносить относительно громоздкий код во внутренний класс со своими методами. По достижении определенного уровня сложности, этот класс мог быть разделен на несколько связанных классов, которые выносились в отдельную папку и нэймспейс. Это позволило привнести ту иерархию в код, которую в стародавние времена обеспечивали локальные функции и процедуры Паскаля.

Таким образом, мое мнение сейчас изменилось - не стоит давать еще одного способа структурирования кода. Это усложнение языка, усложнение чтения (внешний метод становится большим, поэтому сложно охватить его взглядом от начала, и до конца), но нет принципиальных преимуществ.

Tuples

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

Pattern matching, conditions in switch

// type pattern public void Foo(object item) { if (item is string s) { WriteLine(s.Length); } } // Var Pattern public void Foo(object item) { if(item is var x) { WriteLine(item == x); // prints true } } // Wildcard Pattern public void Foo(object item) { if(item is *) { WriteLine("Hi there"); //will be executed } }

Более полный пример можно посмотреть по ссылке.
Выглядит неплохо, но нужно посмотреть, насколько это окажется полезным на практике. Есть интуитивное подозрение, что усложнение языка не окупится тем количеством кейсов, где эта возможность будет полезна. Так что пока я насторожен.

Ref locals and ref returns

static void Main() { var arr = new[] { 1, 2, 3, 4 }; ref int Get(int[] array, int index)=> ref array[index]; ref int item = ref Get(arr, 1); WriteLine(item); item = 10; WriteLine(arr[1]); ReadLine(); } // Will print // 2 // 10

Простое и интуитивно понятное расширение для работы со ссылками. Но реальная нужда в нем пока непонятна.

Описанных далее в статье на Хабре пунктов -Записи- и -Создание неизменяемых объектов- не вижу сейчас в текущих предложениях на 7-ю версию C#, поэтому оценивать их не буду.

Итак, что в итоге?

С моей точки зрения, C# нажил (7-ю версию пока не трогаю, оплакивать буду по факту) себе такие лишние фичи:

  1. Человеко-понятный синтаксис LINQ. Достаточно было бы остановиться на fluent-стиле.
  2. Анонимные типы.
  3. Var. Эта ограниченная локальными переменными фича не дала внедрить нормального определения типов, в то же время существенно ухудшив читабельность кода.
  4. Импорт статиков - ухудшает читаемость кода.

Что было особенно полезно:

  1. LINQ (без ЧПС).
  2. Лямбды.
  3. Постепенное упрощение инициализации и описания объектов, различных структур данных, свойств.
  4. Именованные и по умолчанию параметры.
  5. Async/await.
  6. Интерполяция строк.

Источник: habrahabr.ru

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