Функциональный C#. Часть 1. Неизменяемые объекты


Оглавление (нажмите, чтобы открыть):

Необязательные и именованные аргументы

C# — Руководство по C# — Необязательные и именованные аргументы

Необязательные аргументы

В версии C# 4.0 внедрено новое средство, повышающее удобство указания аргументов при вызове метода. Это средство называется необязательными аргументами и позволяет определить используемое по умолчанию значение для параметра метода. Данное значение будет использоваться по умолчанию в том случае, если для параметра не указан соответствующий аргумент при вызове метода. Следовательно, указывать аргумент для такого параметра не обязательно. Необязательные аргументы позволяют упростить вызов методов, где к некоторым параметрам применяются аргументы, выбираемые по умолчанию. Их можно также использовать в качестве «сокращенной» формы перегрузки методов.

Главным стимулом для добавления необязательных аргументов послужила необходимость в упрощении взаимодействия с объектами СОМ. В нескольких объектных моделях Microsoft (например, Microsoft Office) функциональность предоставляется через объекты СОМ, многие из которых были написаны давно и рассчитаны на использование необязательных параметров.

Пример использования необязательных аргументов показан ниже:

Следует иметь в виду, что все необязательные аргументы должны непременно указываться справа от обязательных. Помимо методов, необязательные аргументы можно применять в конструкторах, индексаторах и делегатах.

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

Именованные аргументы

Еще одной функциональной возможностью, которая добавилась в C# с выходом версии .NET 4.0, является поддержка так называемых именованных аргументов (named arguments). Как известно, при передаче аргументов методу порядок их следования, как правило, должен совпадать с тем порядком, в котором параметры определены в самом методе. Иными словами, значение аргумента присваивается параметру по его позиции в списке аргументов.

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

Здесь имя_параметра обозначает имя того параметра, которому передается значение. Разумеется, имя_параметра должно обозначать имя действительного параметра для вызываемого метода.

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

Синтаксис именованных аргументов более многословен, чем у обычных позиционных аргументов, и поэтому для вызова методов чаще всего применяются позиционные аргументы. Но в тех случаях, когда это уместно, именованные аргументы могут быть использованы довольно эффективно.

Как привести объект базового типа к унаследованному типу

16.09.2012, 23:57

Привести обратно к типу унаследованному от Ienumerable
Есть class Items : Ienumerable делаю Items.Where(i => i.s == «») И возвращается.

Как привести переменную типа Object к типу Double?
Как привести переменную типа Object к типу Double?

Как привести к строгому типу не зная заранее имя типа?
Привет! Есть такой код: var my ; Type t = Type.GetType(myClass); var.

Как сделать явное приведения переменной типа класса-наследника к типу базового класса?
Читал, что для этого нужно явное приведение к типу, но на практике выдаёт ошибку. Вот код: using.

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

17.09.2012, 07:40 2 17.09.2012, 10:43 [ТС] 3 17.09.2012, 11:58 4

17.09.2012, 11:58
17.09.2012, 13:56 5
17.09.2012, 14:21 [ТС] 6
17.09.2012, 14:54 7

Вы пытались понять принципы ООП? Они описаны в любой книжке по c#.
Класс-потомок — является классом-предком.
Но предок не обязательно является классом-потомком.

Так как вы написали делать нельзя, и точка. Зачем вам это — остается только догадываться, саму задачу вы тщательно скрываете.

17.09.2012, 15:02 [ТС] 8
18.09.2012, 15:24 9
18.09.2012, 15:46 10

Например есть 2 класса:
Класс №1 — Человек (human) — класс-родитесль
Класс №2 — Рабочий (employee) — класс-потомок

Можно однозначно сказать, что любой рабочий, также является человеком. То есть любой объект класса employee является human.
Но обратное неверно, если у нас есть человек, то он не обязательно рабочий. Существует такой объект human, который не является employee.

18.09.2012, 23:05 11
18.09.2012, 23:14 [ТС] 12

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

19.09.2012, 06:44 13
14.11.2020, 08:24 14

Для меня вопрос актуален

Как привести потомок к предку, при их различии??
Есть класс, нужно добавить данные, что бы потом Serialize в json

14.11.2020, 12:28 15

Ключевое слово «различие«. Так как в потомке, есть отличные от предка свойства, поля и методы, то привести потомка к предку всегда можно отбросив эти отличные свойства и поля.
А как привести предка? Что надо прописать в эти свойства и поля? Оставить их незаполненными или какие-то значения в них прописать? Всего этого компилятор не знает. Для того, чтобы дать эту информацию компилятору, надо в потомке переопределить методы явного и неявного приведения (или один из них) предка к потомку. Тогда Вы сможете приводить предок к потомку, так как среда уже будет знать правила этого приведения.

Добавлено через 5 минут
Вот Вы в классе LangNameLog зачем-то переопределяете приведение этого класса к строке? А зачем? Для этого надо переопределять метод ToString. Для Вашей задачи Вы лучше переопределите приведение к базовому типу, если оно Вам надо.

14.11.2020, 19:07 16

Ключевое слово «различие«. Так как в потомке, есть отличные от предка свойства, поля и методы, то привести потомка к предку всегда можно отбросив эти отличные свойства и поля.
А как привести предка? Что надо прописать в эти свойства и поля? Оставить их незаполненными или какие-то значения в них прописать? Всего этого компилятор не знает. Для того, чтобы дать эту информацию компилятору, надо в потомке переопределить методы явного и неявного приведения (или один из них) предка к потомку. Тогда Вы сможете приводить предок к потомку, так как среда уже будет знать правила этого приведения.

Добавлено через 5 минут
Вот Вы в классе LangNameLog зачем-то переопределяете приведение этого класса к строке? А зачем? Для этого надо переопределять метод ToString. Для Вашей задачи Вы лучше переопределите приведение к базовому типу, если оно Вам надо.

собственно как это и сделать?

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

что мне имплицит и эксплицид определить? вряд ли мне это поможет т.к. у меня новые значения появляются
я могу конечно конструктор сделать и в нем определить создание нового на основе старого? но может есть спец элементы для этого?

Информационный портал по безопасности

Язык C# почти функционален

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

Многие программисты неявно подразумевают, что «функциональное программирование (ФП) должно реализовываться только на функциональном языке». C# — объектно-ориентированный язык, поэтому не стоит и пытаться писать на нем функциональный код.

Разумеется, это поверхностная трактовка. Если вы обладаете чуть более глубокими знаниями C# и представляете себе его эволюцию, то, вероятно, в курсе, что язык C# мультипарадигмальный (точно как и F#) и что, пусть он изначально и был в основном императивным и объектно-ориентированным, в каждой последующей версии добавлялись и продолжают добавляться многочисленные функциональные возможности.

Итак, напрашивается вопрос: насколько хорош нынешний язык C# для функционального программирования? Перед тем, как ответить на этот вопрос, я поясню, что понимаю под «функциональным программированием». Это парадигма, в которой:

  • Делается акцент на работе с функциями
  • Принято избегать изменения состояния

    Чтобы язык способствовал программированию в таком стиле, он должен:

  • Поддерживать функции как элементы 1-го класса; то есть, должна быть возможность трактовать функцию как любое другое значение, например, использовать функции как аргументы или возвращаемые значения других функций, либо хранить функции в коллекциях
  • Пресекать всякие частичные «местные» замены (или вообще сделать их невозможными): переменные, объекты и структуры данных по умолчанию должны быть неизменяемыми, причем должно быть легко создавать модифицированные версии объекта
  • Автоматически управлять памятью: ведь мы создаем такие модифицированные копии, а не обновляем данные на месте, и в результате у нас множатся объекты. Это непрактично в языке, где отсутствует автоматическое управление памятью

    С учетом всего этого, ставим вопрос ребром:

    Насколько язык C# — функциональный?

    Ну… давайте посмотрим.

    1) Функции в C# — действительно значения первого класса. Рассмотрим, например,
    следующий код:

    Здесь видно, что функции – действительно значения первого класса, и можно присвоить функцию переменной triple, после чего задать ее в качестве аргумента Select.

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

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

    2) В идеале, язык также должен пресекать местные замены. Здесь – самый крупный недостаток C#; все по умолчанию изменяемо, и программисту требуется немало потрудиться, чтобы обеспечить неизменяемость. (Сравните с F#, где переменные по умолчанию неизменяемы, и, чтобы переменную можно было менять, ее требуется специально пометить как mutable.)

    Что насчет типов? Во фреймворке есть несколько неизменяемых типов, например, string и DateTime, но определяемые пользователем изменяемые типы в языке поддерживаются плохо (хотя, как будет показано ниже, ситуация немного исправилась в C#, и в последующих версиях также должна улучшаться). Наконец, коллекции во фреймворке являются изменяемыми, но уже имеется солидная библиотека неизменяемых коллекций.

    3) С другой стороны, в C# выполняется более важное требование: автоматическое управление памятью. Таким образом, хотя язык и не стимулирует стиль программирования, не допускающий местных замен, программировать в таком стиле на C# удобно благодаря сборке мусора.

    Итак, в C# очень хорошо поддерживаются некоторые (но не все) приемы функционального программирования. Язык эволюционирует, и поддержка функциональных приемов в нем улучшается.

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

    Функциональная сущность LINQ

    Когда вышел язык C# 3 одновременно с фреймворком .NET 3.5, там оказалась масса возможностей, по сути заимствованных из функциональных языков. Часть из них вошла в библиотеку ( System.Linq ), а некоторые другие возможности обеспечивали или оптимизировали те или иные черты LINQ – например, методы расширения и деревья выражений.


    В LINQ предлагаются реализации многих распространенных операций над списками (или, в более общем виде, над «последовательностями», именно так с технической точки зрения нужно называть IEnumerable ); наиболее распространенные из подобных операций – отображение, сортировка и фильтрация. Вот пример, в котором представлены все три:

    Обратите внимание, как Where, OrderBy и Select принимают другие функции в качестве аргументов и не изменяют полученный IEnumerable, а возвращают новый IEnumerable, иллюстрируя оба принципа ФП, упомянутые мною выше.

    LINQ позволяет запрашивать не только объекты, находящиеся в памяти (LINQ to Objects), но и различные иные источники данных, например, SQL-таблицы и данные в формате XML. Программисты, работающие с C#, признали LINQ в качестве стандартного инструментария для работы со списками и реляционными данными (а на такую информацию приходится существенная часть любой базы кода). С одной стороны это означает, что вы уже немного представляете, каков из себя API функциональной библиотеки.

    С другой стороны, при работе с иными типами специалисты по C# обычно придерживаются императивного стиля, выражая задуманное поведение программы в виде последовательных инструкций управления потоком. Поэтому большинство баз кода на C#, которые мне доводилось видеть – это чересполосица функционального (работа с IEnumerable и IQueryable ) и императивного стиля (все остальное).

    Таким образом, хотя C#-программисты и в курсе, каковы достоинства работы с функциональной библиотекой, например, с LINQ, они недостаточно плотно знакомы с принципами устройства LINQ, что мешает им самостоятельно использовать такие приемы при проектировании.
    Это – одна из проблем, решить которые призвана моя книга.

    Функциональные возможности в C#6 и C#7

    Пусть C#6 и C#7 и не столь революционны, как C#3, эти версии привносят в язык множество мелких изменений, которые в совокупности значительно повышают удобство работы и идиоматичность синтаксиса при написании кода.

    ПРИМЕЧАНИЕ: Большинство нововведений в C#6 и C#7 оптимизируют синтаксис, а не дополняют функционал. Поэтому, если вы работаете со старой версией C#, то все равно сможете пользоваться всеми приемами, описанными в этой книге (разве что ручной работы будет чуть больше). Однако, новые возможности значительно повышают удобочитаемость кода, и программировать в функциональном стиле становится приятнее.

    Рассмотрим, как эти возможности реализованы в нижеприведенном листинге, а затем обсудим, почему они важны в ФП.

    Листинг 1. Возможности C#6 и C#7, важные в контексте функционального программирования

    1. using static обеспечивает неквалифицированный доступ к статическим членам System.Math , например, PI и Pow ниже
    2. Авто-свойство getter-only можно установить только в конструкторе
    3. Свойство в теле выражения
    4. Локальная функция – это метод, объявленный внутри другого метода
    5. Синтаксис кортежей C#7 допускает имена членов

    Импорт статических членов при помощи «using static»

    Инструкция using static в C#6 позволяет “импортировать” статические члены класса (в данном случае речь идет о классе System.Math ). Таким образом, в нашем случае можно вызывать члены PI и Pow из Math без дополнительной квалификации.

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

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

    Более простые неизменяемые типы с getter-only авто-свойствами

    При объявлении getter-only авто-свойства, например, Radius , компилятор неявно объявляет readonly резервное поле. В результате значение этим свойствам может быть присвоено лишь в конструкторе или внутристрочно.

    Getter-only автосвойства в C#6 облегчают определение неизменяемых типов. Это видно на примере класса Circle: в нем есть всего одно поле (резервное поле Radius ), предназначенное только для чтения; итак, создав Circle , мы уже не можем его изменить.

    Более лаконичные функции с членами в теле выражения

    Свойство Circumference объявляется вместе с «телом выражения», которое начинается с => , а не с обычным «телом инструкции», заключаемым в < >. Обратите внимание, насколько лаконичнее этот код по сравнению со свойством Area!

    В ФП принято писать множество простых функций, среди которых полно однострочных. Затем такие функции компонуются в более сложные рабочие управляющие потоки. Методы, объявляемые в теле выражения, в таком случае сводят к минимуму синтаксические помехи. Это особенно наглядно, если попробовать написать функцию, которая возвращала бы функцию – в книге это делается очень часто.

    Синтаксис с объявлением в теле выражения появился в C#6 для методов и свойств, а в C#7 стал более универсальным и применяется также с конструкторами, деструкторами, геттерами и сеттерами.

    Если приходится писать множество простых функций, это означает, что зачастую функция вызывается всего из одного места. В C#7 это можно запрограммировать явно, объявляя методы в области видимости метода; например, метод Square объявляется в области видимости геттера Area .

    Оптимизированный синтаксис кортежей

    Пожалуй, в этом заключается важнейшее свойство C#7. Поэтому можно с легкостью создавать и потреблять кортежи и, что самое важное, присваивать их элементам значимые имена. Например, свойство Stats возвращает кортеж типа (double, double) , но дополнительно задает значимые имена для элементов кортежа, и по ним можно обращаться к этим элементам.

    Причина важности кортежей в ФП, опять же, объясняется все той же тенденцией: разбивать задачи на как можно более компактные функции. У на может получиться тип данных, применяемый для захвата информации, возвращаемой всего одной функцией и принимаемой другой функцией в качестве ввода. Было бы нерационально определять для таких структур выделенные типы, не соответствующие никаким абстракциям из предметной области – как раз в таких случаях и пригодятся кортежи.

    В будущем язык C# станет функциональнее?

    Когда в начале 2020 года я писал черновик этой главы, я с интересом отметил, что все возможности, вызывавшие у команды разработчиков «сильный интерес», традиционно ассоциируются с функциональными языками. Среди этих возможностей:

    • Регистрируемые типы (неизменяемые типы без трафаретного кода)
    • Алгебраические типы данных (мощное дополнение к системе типов)
    • Сопоставление с шаблоном (напоминает оператор `switch` переключающий “форму” данных, например, их тип, а не только значение)
    • Оптимизированный синтаксис кортежей

    Однако, пришлось довольствоваться лишь последним пунктом. В ограниченном объеме реализовано и сопоставление с шаблоном, но пока это лишь бледная тень сопоставления с шаблоном, доступного в функциональных языках, и на практике такой версии обычно недостаточно.

    С другой стороны, такие возможности планируются в следующих версиях, и уже идет проработка соответствующих предложений. Таким образом, в будущем мы, вероятно, увидим в C# регистрируемые типы и сопоставление по шаблону.

    Итак, C# и далее будет развиваться как мультипарадигмальный язык со все более выраженным функциональным компонентом.

  • Да, нужна именно такая специализированная книга по ФП в C#
    Нужна более обзорная книга по C# 7
    До сих пор нужна книга Стивена Клири Concurrency in C#, а никто никак не переведет
    Нужна книга по F#

    Проголосовало 40 человек. Воздержалось 12 человек.

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

    Функциональный тип в C#. Делегаты

    Как определяется функциональный тип и как появляются его экземпляры

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

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

    Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата ? Как и у всякого класса, есть две возможности:

    • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
    • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса.

    Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата . Классы, согласные с контрактом , должны объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата . Если контракт выполняется, то можно создать экземпляры делегата , присвоив им в качестве значений функции, удовлетворяющие контракту . Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра — object , а у экземпляра соответствующий параметр имеет тип, согласованный с object , например, int .

    Начнем примеры этой лекции с объявления трех делегатов . Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс :

    Прокомментирую этот текст.

    • Первым делом объявлены три функциональных класса — три делегата : Proc , MesToPers , Fun1 . Каждый из них описывает множество функций фиксированной сигнатуры.
    • В классе OwnDel описаны четыре метода: Plus , Minus , Plus1 , Minus1 , сигнатуры которых соответствуют сигнатурам, задаваемых классами Proc и Fun1 .
    • Поля p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1 .
    • В конструкторе класса поля p1 и f1 связываются с конкретными методами Plus или Minus , Plus1 или Minus1 . Связывание с той или иной функцией в данном случае определяется значением поля sign .

    Заметьте, экземпляры делегатов можно рассматривать как ссылки (указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в этой роли выступают не обычные объекты, имеющие поля, а методы, задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к С++.

    Приведу теперь процедуру, тестирующую работу созданного класса:

    Клиент класса OwnDel создает экземпляр класса, передавая конструктору знак той операции , которую он хотел бы выполнить над своими счетами — account и account1 . Вызов p1 и f1 , связанных к моменту вызова с закрытыми методами класса, приводит к выполнению нужных функций.

    В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим многократно встречавшийся класс Person , слегка изменив его определение :

    Класс Person устроен обычным способом: у него несколько перегруженных конструкторов, закрытые поля и процедуры-свойства для доступа к ним. Особо обратить внимание прошу на метод класса ToPerson , сигнатура которого совпадает с сигнатурой класса, определенной введенным ранее делегатом MesToPers . Посмотрите, как клиент класса может связать этот метод с экземпляром делегата , определенного самим клиентом:

    Обратите внимание, что поскольку метод ToPerson не является статическим методом, то при связывании необходимо передать и объект , вызывающий метод. Более того, переданный объект становится доступным экземпляру делегата . Отсюда сразу же становится ясным, что экземпляры делегата — это не просто указатели на функцию, а более сложно организованные структуры. Они, по крайней мере, содержат пару указателей на метод и на объект , вызвавший метод. Вызываемый метод в своей работе использует как информацию, передаваемую ему через аргументы метода, так и информацию, хранящуюся в полях объекта. В данном примере переданное сообщение «пора работать» присоединится к имени объекта, и результирующая строка будет выдана на печать . В тех случаях, когда метод, связываемый с экземпляром делегата , не использует информацию объекта, этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими, так и динамическими методами, связанными с конкретными объектами.

    Последние три строки были добавлены в вышеприведенную тестирующую процедуру. Взгляните на результаты ее работы.

    Функциональное программирование в Nemerle

    Автор: Чистяков Влад (VladD2)
    The RSDN Group
    Источник: RSDN Magazine #4-2006

    Опубликовано: 03.03.2007
    Исправлено: 10.12.2020
    Версия текста: 1.0

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

    Хотя статья опирается на Nemerle, она может оказаться интересной и для тех, кто не интересуется этим языком, но заинтересован в освоении функционального программирования и аспектах, с ним связанным.

    Парадокс Блаба

    Прежде чем начать рассказывать о том, что Nemerle дает программисту, мне очень хочется привести цитату из статьи Пола Грейхема «Lisp: побеждая посредственность». Конечно, Пол Грейхем является проповедником Lisp-а, а о Nemerle он вообще на тот момент и слышать не мог. Но, тем не менее, его слова как нельзя более точно описывают, почему многим программистам трудно понять и оценить более мощный язык (полную версию статьи вы можете найти по адресу http://www.nestor.minsk.by/sr/2003/07/30710.html).

    Парадокс Блаба

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

    И причина того, что все вокруг пишут не на Lisp’е, заключается в том, что выбор языка программирования — вопрос не только технологии, но также и привычки, а ничто не меняется так медленно, как привычки. Конечно, оба эти тезиса требуют разъяснений.

    Я начну с шокирующего утверждения: языки программирования отличаются друг от друга своей мощностью.

    По крайней мере, мало кто будет спорить, что высокоуровневые языки мощнее машинного языка (машинный код и ассемблеры – VladD2). Большинство программистов согласятся, что, как правило, программировать стоит не на машинном языке, а на каком-нибудь языке высокого уровня, переводя программу в машинный код с помощью компилятора. Сейчас эта идея получила даже аппаратное воплощение — с восьмидесятых годов команды процессоров разрабатываются скорее для компиляторов, чем для программистов.

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

    Все языки одинаково мощны, если рассматривать их с точки зрения эквивалентности машине Тьюринга, но это не та мощь, которая важна программисту. (Никто ведь не хотел бы программировать машину Тьюринга). Мощь языка, в которой заинтересован программист, возможно, трудно определить формальными методами, однако одно из объяснений этого понятия заключается в свойствах, которые в менее мощном языке можно получить, только написав на нем интерпретатор для более мощного языка. Если в языке A есть оператор для удаления пробелов из строк, а в языке B его нет, это не делает A более мощным, чем B, так как в B можно написать процедуру, которая делала бы это.

    Но, скажем, если язык A поддерживает рекурсию, а B — нет, это нечто, что нельзя исправить написанием библиотечных функций (VladD2: впрочем, бывает так, что средствами языка B в принципе можно решить проблему, но решение ее столь сложно, что решение это можно назвать чисто теоретическим).


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

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

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

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

    Понятно, что уровень машинного языка очень низок. А высокоуровневые языки часто рассматриваются как одинаковые, по крайней мере, так принято считать. Но это не так. Технический термин «язык программирования высокого уровня» не обозначает ничего определенного. Не существует четкой границы между множеством «машинных» языков с одной стороны, и множеством «высокоуровневых» – с другой. Языки распределены в континууме (возможно, не просто континууме, а некой структуре, уменьшающейся кверху; важна здесь не форма, а сама идея о том, что существует, по крайней мере, частичный порядок) абстрактности, начиная от самых мощных «языков высокого уровня» вниз к «машинным языкам», которые, в свою очередь, тоже отличаются друг от друга по мощности.

    Возьмем Cobol. Cobol — язык высокого уровня, так как компилируется в машинный язык. Но станет ли кто-нибудь утверждать, что по мощности Cobol эквивалентен, скажем, Python-у? (VladD2: учитывая свой опыт общения на форумах RSDN скажу, что, несомненно, станет:) ) Возможно, он ближе к машинному языку, чем Python.

    А как насчет Perl четвертой версии? В Perl 5 в язык были добавлены лексические замыкания (lexical closures). Большинство Perl-хакеров согласятся, что Perl 5 мощнее, чем Perl 4. Но раз вы это признали, вы признали, что один высокоуровневый язык может быть мощнее другого. Из этого неизбежно следует, что использовать нужно самый мощный язык.

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

    Программисты очень привязываются к своим любимым языкам, а я не хочу оскорбить ничьи чувства, поэтому я объясню свою позицию, используя гипотетический язык с названием Блаб.

    Блаб попадает в середину континуума абстрактности. Это не самый мощный язык, но он мощнее, чем Cobol или машинный язык.

    И на самом деле, наш гипотетический программист на Блабе не будет использовать ни Cobol, ни машинный код. Для машинных кодов есть компиляторы. Что же касается Cobol-а, наш программист не знает, как на этом языке вообще что-то можно сделать. В Cobol-е ведь даже нет возможности X, присутствующей в Блабе!

    Когда наш гипотетический Блаб-программист смотрит вниз на континуум мощности языков, он знает, что смотрит вниз. Менее мощные, чем Блаб, языки явно менее мощны, так как в них нет некой особенности, к которой привык программист. Но когда он смотрит в другом направлении, вверх, он не осознает, что смотрит вверх. То, что он видит, — это просто «странные» языки. Возможно, он считает их одинаковыми с Блабом по мощности, но со всяческими сложными штучками. Блаба для нашего программиста вполне достаточно, так как он думает на Блабе (VladD2: выделено мной).

    Когда мы поменяем точку обзора программиста, используя любой язык программирования выше по континууму мощности, мы обнаружим, что теперь программист смотрит на Блаб сверху вниз. «Как же можно что-то сделать, используя Блаб? В нем отсутствует даже конструкция Y!»

    Используя метод индукции, приходишь к выводу, что только те программисты, которые понимают самый мощный язык, в состоянии осознать полную картину разницы в мощности между различными языками (видимо, именно это имел в виду Эрик Реймонд, когда говорил о том, что Lisp сделает вас лучше как программиста). Следуя парадоксу Блаба, нельзя доверять мнению других: другие программисты довольны тем языком, который используют, потому что этот язык определяет способ их программистского мышления.

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

    Пять языков, которые советует хакерам Эрик Реймонд, находятся в разных точках континуума мощности, и то, где они находятся относительно друг друга, — тонкий вопрос. Я скажу, что Lisp находится на вершине континуума. И чтобы поддержать это утверждение, я скажу о том, чего мне не хватает, когда я смотрю на остальные пять языков. Как же можно что-то сделать с ними, думаю я, без свойства Z? И самое большое Z — это макросы. (VladD2: Рассматривать макросы как отдельное свойство — это немного неправильно. На практике их польза увеличивается такими свойствами Lisp’а, как лексические замыкания и частичная параметризация (rest parameters) – аналогичная возможность в Nemerle назевается «частичное применение (partial application)).

    Во многих языках есть что-то, называющееся макросом. Но макросы в Lisp’е уникальны (VladD2: на сегодня это так, только если глядеть на другие языки с высоты Lisp-а, но об этом позже). То, что делают макросы, имеет отношение, верите вы или нет, к скобкам. Создатели Lisp’а добавили все эти скобки в язык не для того, чтобы отличаться от других. Скобки в Lisp’е имеют особый смысл, они — внешнее свидетельство фундаментальной разницы между Lisp’ом и другими языками.

    Программа на Lisp’е состоит из данных. И не в том тривиальном значении, что исходные файлы содержат символы, а строки — один из типов данных, поддерживаемых языком. После прочтения программы парсером Lisp-код состоит из готового к использованию дерева структур данных.

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

    Программы, которые пишут программы? И когда же такое может понадобиться?

    Не очень часто, если вы думаете на Cobol’е. И постоянно, если вы думаете на Lisp’е. Было бы удобно, если бы я дал пример мощного макроса и сказал бы: «Вот! Смотрите!». Но если бы я и привел пример, для того, кто не знает Lisp, он выглядел бы как белиберда. Рамки данной статьи не позволяют изложить все необходимое для понимания подобного примера. В книге Ansi Common Lisp я старался излагать материал как можно быстрее, но даже так я не добрался до макросов раньше страницы 160.

    Однако мне кажется, что я могу дать убедительный аргумент. Исходный текст редактора Viaweb на 20-25 процентов состоял из макросов. Макросы сложнее писать, чем обычные функции Lisp’а, и считается дурным тоном использовать их там, где можно без них обойтись. Поэтому каждый макрос в той программе был необходим. Это значит, что примерно 20-25 процентов кода в программе делают то, что нельзя просто сделать на других языках.

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

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

    К чему я привел этот фрагмент? Дело в том, что если вы думаете на C# или, скажем, на Delphi, или даже на C++, то без желания понять и некоторых усилий с вашей стороны вы окажетесь в роли того самого Блаб-программиста, смотрящего на более мощный язык и видящего в нем не более чем кучу непонятных заморочек.

    Грэхем немного лукавит. Реальная жизнь сложнее описанной им модели. Языки программирования невозможно четко расположить на шкале, где нулем является машинный язык, а максимальным значением – «самый-самый» язык программирования. Фактически, есть много языков, которые превосходят другие в том или ином аспекте, и уступают в прочих. Тот же Lisp не безгрешен. Причем достоинства языка зачастую являются причиной его недостатков. Так, расплатой за мощные макросы является совершенно ужасный синтаксис без приоритетов операторов (да и без операторов как таковых), без внятно выделяющиеся синтаксических конструкций и т.п. Причем макросы Lisp-а, ко всему прочему, еще и ограничены в своих возможностях, так как не позволяют при своей работе использовать информацию о типах и иным образом взаимодействовать с компилятором (его в большинстве случаев просто нет).

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

    Вспомните те времена, когда вы изучали что-то действительно новое для вас. Помните напряжение мозга, возникающее в тщетных попытках понять что-то концептуально новое? А помните то чувство удивленного восторга после того, как вы смогли понять это новое? Помните слова: «Боже, как это просто! И чего я не мог тут понять?!»?

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

    Функции как первоклассные объекты

    Для начала приведу перевод термина «первоклассный объект» (First-class object) из Википедии:

    В компьютерной области, первоклассный объект (или значение, сущность, гражданин) в контексте конкретного языка программирования – это сущность, которую можно использовать в коде без ограничений (по сравнению с другими объектами в том же языке). В зависимости от языка это может значить, что первоклассный объект:

    • Может выражаться в виде анонимного литерального значения.
    • Может быть помещен в переменную.
    • Может быть помещен в структуры данных, поддерживаемые в языке.
    • Его можно отличать от других объектов того же класса (независимо от того, если ли у него имя).
    • Может быть проверен на равенство с другими объектами того же класса.
    • Может быть передан в качестве параметра в некоторую функцию (метод, процедуру).
    • Может быть возвращен из функции.
    • Может быть создан во время выполнения (в runtime).

    От себя могу сказать что, пожалуй, главным в понятии «первоклассный объект» является то, что им можно полноценно манипулировать (и манипулировать удобно).

    Большинство современных языков рассматривают функцию как почти первоклассный объект. Казалось бы, мы можем передавать функции (ссылки на них) в другие функции, возвращать из них и даже помещать в структуры данных (это называется поддержкой функций высшего порядка – ФВП ), но к сожалению мы все еще кое в чем ограничены. Пожалуй, ближе всего (из господствующих (mainstream) языков) к использованию функций как первоклассных объектов подошел C# 2.0. Однако и в нем манипулировать функциями не очень удобно. К тому же, по сравнению с другими подходами, подход, основанный на манипуляции функциями, порождает более медленный код. Nemerle предоставляет более широкий набор возможностей манипуляции функциями. Причем манипуляция функциями в этом языке выглядит более просто и естественно. Так, в C# мы, в сущности, манипулируем не функциями, а делегатами, которые являются отдельными объектами, инкапсулирующими ссылку на функцию и ее контекст. При этом делегаты обладают некоторой избыточностью (они позволяют ссылаться более чем на одну функцию, и каждое объявление делегата вводит новый тип), которая затрудняет манипуляцию функциями.

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

    1. Передавать функции в качестве параметров других функций и методов.
    2. Возвращать функции как возвращаемое значение функций и методов.
    3. Создавать локальные функции.
    4. Создавать безымянные функции – лямбды.
    5. Частично применять функции, тем самым создавая новые функции из старых, задавая некоторые значения в качестве их параметров.
    6. Использовать в локальных функциях, лямбдах и частично примененных функциях замыкания на контекст, в котором они определены.
    7. Возвращать из функций множество значений разных типов (кортежи).
    8. Помещать функции в переменные.
    9. Создавать списки функций и помещать их в другие структуры данных.
    10. Применять функции к спискам значений или даже спискам функций (получая тем самым списки, содержащие измененные функции).
    11. Использовать концевую рекурсию, не боясь получить переполнение стека или проблем с производительностью, что дает возможность чаще пользоваться рекурсивными алгоритмами. Компилятор гарантирует, что концевая рекурсия будет развернута в цикл.

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

    Начнем с локальных функций и замыканий. Многие из вас уже сталкивались с локальными функциями в Паскале и его потомках. Главное отличие локальных функций Nemerle от их аналогов в Паскале-подобных языках – это то, что локальные функции могут располагаться в любом месте метода и использовать объявленные выше переменные и функции. Это позволяет производить декомпозицию сложных методов, причем делать это проще и с меньшими затратами сил, нежели при использовании для этого методов и объектов.

    Замыкания родились очень давно. Это произошло еще во времена раннего Lisp-а. Однако до недавнего времени они упорно обходили стороной господствующие языки программирования.

    Фактически первым из господствующих ЯП, в котором появились замыкания, стал C# 2.0. Правда, доступны они только в рамках анонимных методов, которые ввиду неудобного синтаксиса, необходимости манипулировать анонимными методами исключительно через делегаты и относительной медленности делегатов, применяются на практике довольно редко. Кроме того, бледное подобие замыканий можно наблюдать в lambda из библиотеки Boost (C++). Но из-за сильной ограниченности ее вряд ли можно рассматривать всерьез.

    Nemerle поддерживает замыкания для лямбд (аналогов анонимных методов C#, но с человеческим лицом, в C# 3.0 тоже появится более удобный синтаксис, который так и будет называться – лямбдой) и локальных функций.

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

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

    ПРИМЕЧАНИЕ

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

    Функции высшего порядка (ФВП)

    Я долго думал, какие примеры использовать для демонстрации преимущества функционального подхода и пришел к выводу, что лучше всего воспользоваться идей Кернигана. В своей книге «The C Programming language» он в качестве примеров приводил реализацию функций из стандартной библиотеки языка C.

    Пожалуй, первым случаем применения ФВП в моей жизни была функция qsort (функция быстрой сортировки). Вот ее объявление на C++:

    В последнем своем параметре эта функция получает ссылку на другую функцию – compare. Функция compare применяется в ходе выполнения алгоритма быстрой сортировки для сравнения элементов сортируемого массива.

    Аналогичная функция имеется в .NET Framework 1.x:

    Здесь вместо функции сравнения используется интерфейс IComparer. Использование объекта-компаратора вместо простой ссылки на функцию является не очень удобным решением.

    ПРИМЕЧАНИЕ

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

    Поэтому в .NET 2.0 была добавлена следующая версия функции сортировки:

    Эта функция использует для сравнения элементов делегат. Это позволяет использовать расширенные возможности C# 2.0 для объявления функций сравнения по месту:

    Какие проблемы есть у этого кода? Во-первых, код несколько длинноват. Во-вторых, он будет работать медленнее аналогичного варианта, использующего объект-компаратор.

    Функциональные типы

    В Nemerle вместо делегатов можно использовать функциональный тип. В отличие от делегатов, его не нужно объявлять отдельно, и не требуется создавать его экземпляров. Любая функция (в том числе и лямбда) с подходящей сигнатурой может быть передана в параметр функционального типа. Вот как будет выглядеть аналогичная функция сортировки на Nemerle:

    T * T -> int – это описание функционального типа. Функция, соответствующая этому типу, должна принимать два параметра типа T и возвращать целочисленное значение.

    А вот так выглядит применение данной функции (пример полностью аналогичен коду на C#):

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

    Многие могут задаться вопросом «почему же скорость вызова функционального объекта выше, чем у интерфейса?». Это происходит из-за того, что функциональные объекты преобразуются компилятором Nemerle в классы с виртуальным методом apply(. ), список параметров которого аналогичен списку параметров лямбды. Так, если в коде вы воспользовались лямбдой или частичным применением оператора, как в следующем примере:

    компилятор сгенерирует класс вида:

    и следующим образом модифицирует код программы:

    Таким образом, скорость вызова функционального объекта соответствует скорости вызова виртуального метода (в перспективе JIT-компилятор .NET или компилятор Nemerle могут оптимизировать этот вызов, заменяя его не виртуальным или вообще устраняя).

    Виртуальный же метод в .NET быстрее вызова метода интерфейса и тем более быстрее вызова делегата. Разница эта не так велика. Она варьируется, в зависимости от версии .NET Framework и марки процессора, в пределах от нескольких процентов (для .NET Framework 2.0.50727.308 с маркетинговым названием .NET 3.0) и Core 2 Duo, до 2-3 раз в более ранних версиях .NET Framework и на более слабых процессорах. Подробности того, почему скорость вызова виртуальных методов в .NET выше, чем скорость вызова методов интерфейсов и делегатов, выходит за рамки этой статьи. Вы можете найти их на форумах и в статьях на RSDN.ru.

    Я провел смелый научный эксперимент, суть которого заключалась в измерении скорости быстрой сортировки с использованием разных типов компараторов. Вот результаты этого теста:

    Здесь «Sort Fun» это тест, использующий для сравнения элементов функциональный объект, «Sort Delegate» – делегат, «Sort Interface» – объект-компаратор, используемый через интерфейс, «Sort Int» – мономорфная реализация сортировки для массива целых, «Array.Sort» – результат использования встроенной в .NET Framework функции (без компаратора, написанной на C++), а «Array.Sort + Delegate» – использование встроенной функции с делегатом в качестве компаратора (в данном случае это C#-реализация). Исходные коды теста можно найти по ссылке: http://rsdn.ru/Forum/Message.aspx?m >

    Локальные функции

    Еще больше улучшить читаемость кода можно, явно объявив функции-компараторы:

    При этом код становится самодокументируемым, и необходимость в комментариях отпадает.

    Точно так же можно определить специализированные функции сортировки (ведь мы можем обращаться с функциями так же, как с любыми другими объектами!).

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

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

    Замыкания

    Однако приведенное использование функциональных объектов является самым простым случаем их использования. Вот более сложный пример, демонстрирующий использование замыканий. Предположим, что у нас есть эталонный экземпляр класса Person, и нам нужно узнать, есть ли в неком списке другой экземпляр с таким же именем и фамилией. Если язык не поддерживает замыканий, то информацию об этом объекте придется поместить в специализированный объект компаратор. Для этого придется создать специальный класс, его экземпляр и поместить туда эталонный экземпляр Person. Используя замыкания, то же самое можно сделать по месту:


    Этот код выведет на консоль true, так как в массиве есть искомый элемент. Основная суть этого кода заключается в следующем фрагменте:

    Переменная «y» – это параметр лямбда-функции (анонимной функции, объявленной по месту). Он будет поочередно сопоставляться с каждым элементом массива. А переменная «x» захватывается из внешнего контекста. В конечном итоге вместо приведенного фрагмента получается функция, замкнутая на контексте, в котором она определена. Это позволяет ей использовать все видимые в этом месте переменные. Такую функцию называют лексически замкнутой, или просто замыканием.

    Конечно, лексическое замыкание – это виртуальное понятие для человека. Фактически компилятор тем или иным способом переписывает код. Компиляторы Nemerle и C# 2.0-3.0 при подобном переписывании создают скрытые методы (если замыкание использует только члены текущего класса) или отдельный класс (если замыкания ссылаются на локальные переменные). Во втором случае локальный код переписывается так, чтобы он вместо локальных переменных использовал переменные экземпляра класса. Этот экземпляр также добавляется компилятором. Ссылка на него помещается в локальную переменную.

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

    Примеры ФВП

    В библиотеках Nemerle существует множество функций, принимающих уточняющие функции в качестве параметров. Особенно они удобны при работе со списками и массивами. Вот наиболее употребляемые функции высшего порядка (принимающие уточняющие функции) из класса list[T]:

    Это неполный список, к тому же вы можете расширять его самостоятельно. С использованием методов расширения это делается элементарно.

    Примеры использования функциональных объектов и частичного применения

    Этот код выводит на консоль:

    СОВЕТ

    Компилятор Nemerle считает, что исходный файл имеет кодировку UTF-8. Так что если вы будете пробовать запустить этот пример под Microsoft VS, не забудьте пересохранить файл с кодом «File -> Save ИмяФайла.n as . » и в выпадающем списке на кнопке Save выбрать пункт «Save with Encoding. », а в выпадающем списке выбрать «Encoding» пункт «Unicode (UTF-8 with signature) – Codepage 65001».

    Реальный пример декомпозиции функций

    Функции высшего порядка могут применяться не только в библиотеках, но и в прикладном коде. Вкупе с замыканиями они могут избавить вас от дублирования кода и сделать код более читабельным. Единственное, что вам придется сделать – это задуматься над тем, как и где их лучше использовать.

    Ниже приведен пример функции, из проекта интеграции Nemerle в VS .NET, проверяющей парность скобок:

    ПРИМЕЧАНИЕ

    Конструкция and применена в данном примере вместо конструкции def. Это позволяет объявленной таким образом локальной функции быть видимой в предыдущей локальной функции.

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

    намного понятнее при чтении, чем:

    Используя же Nemerle, я, не задумываясь, вынес общий участок кода в отдельную локальную функцию, а отличающийся фрагмент кода передал в эту функцию в качестве параметра (функционального типа). Поскольку код отличался всего на один оператор, я воспользовался частичным применением операторов для превращения оператора в функцию. В частичном применении можно задать часть параметров/операндов конкретными значениями, а не заданные параметры/операнды обозначить символом-заместителем «_». Кстати, «_» (подчеркивание) играет в Nemerle большую роль. Оно участвует в сопоставлении с образцом, выступает в качестве указания того, что параметр намеренно не используется, и в тех местах, где нужно проигнорировать возвращаемое значение функции (в отличие от C#, Nemerle ругается, если вы игнорируете возвращаемое значение функции, это предотвращает ошибки «по забывчивости»).

    Инверсия управления

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

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

    Проблема элементарно решается с использованием ФВП. Достаточно описать процедуру извлечения данных в виде функционального объекта и передать ее единой универсальной функции. Тогда функция произведет необходимые вычисления, обратится к хранилищу посредством переданного функционального объекта и затем обработает полученные результаты.

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

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

    Свойства vs. неизменяемые поля

    У C#-программистов считается дурным тоном давать публичный доступ к полям классов и структур. Объясняется это очень просто – это нарушает инкапсуляцию и потенциально может привести к скрытому и неконтролируемому изменению состояния объекта, имеющего публичные поля.

    Откровенно говоря, меня всегда коробил тот факт, что между (C#):

    семантическая разница очень умозрительная, а разница в объеме кода очень даже осязаемая и отнюдь не радующая.

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

    Но того же эффекта можно добиться, если сделать поле доступным только для чтения. Конструкции:

    для внешнего кода практически семантически эквивалентны.

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

    Учитывая все это, в Nemerle все переменные было решено по умолчанию сделать неизменяемыми. Напротив, чтобы сделать поле изменяемым, его нужно специально пометить ключевым словом mutable.

    Неизменяемые объекты

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

    Одной из интересных черт неизменяемых (immutable) объектов является то, что ссылки на них можно хранить без каких-либо опасений. Это позволяет использовать один и тот же объект в разных иерархиях объектов. Классическим случаем такого неизменяемого объекта является однонаправленный связанный список, или просто «список». В Nemerle список описывается типом list[T] и является вариантом (здесь и далее под «вариантом» понимается тип данных variant) с неизменяемыми полями (напоминаю, что в Nemerle все поля, не помеченные ключевым словом mutable, являются доступными только для чтения). Вот его упрощенное описание:

    Если, например, создать следующие списки:

    то в памяти они будут представлены следующим образом:

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

    Таким образом, образование списков путем прибавления нового элемента в начало другого списка не приводит к излишнему расходу памяти и не изменяет логической целостности других списков.

    Несмотря на то, что фактически списки превращаются в дерево, для пользователя они выглядят как набор отдельных списков.

    Тот же эффект проявляется и в более сложных структурах данных, например, в деревьях. Неизменяемость элементов позволяет добиться эффекта версионности. Добавляя новые элементы в дерево, мы вынуждены заменять все ветки от заменяемой до корневой ветки включительно. Казалось бы лишние расходы?! Но глубина сбалансированного двоичного дерева обычно не бывает очень большой. Зато неизменность предыдущих веток позволяет иметь одновременно в памяти множество разных версий дерева. Причем затраты на их создание значительно ниже, чем если копировать все дерево целиком.

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

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

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

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

    Такой поход не только позволяет реализовать дешевое копирование деревьев, но и приводит к существенной экономии памяти, ведь большинство элементов деревьев являются общими.

    Фактически, используя неизменяемые структуры данных, можно создать нечто вроде транзакционной памяти. Тот факт, что хранение старых версий стоит дешево, позволяет хранить старые версии объектов и возвращаться к ним, если требуется откат.

    Эта реализация доступна в стандартной библиотеке Nemerle. Ее полное имя – Nemerle.Collections.Tree.Node, где Tree – это имя модуля, а Node – варианта, описывающего дерево. Однако использовать ее напрямую неудобно. Намного удобнее воспользоваться объектно-ориентированными обертками, классами Map[TKey, TValue] и Set[T] из того же пространства имен.

    Функциональные структуры данных активно используются в компиляторе Nemerle. Так, для хранения контекста «открытых» (с помощью директивы using) пространств имен используется экземпляр типа GlobalEnv, который содержит неизменяемые списки открытых пространств имен и alias-ов, Set[string] для хранения ключевых слов, Map[string, GrammarElement] для хранения операторов, запускающих механизм синтаксических расширений, и Map[string, MainParser.OperatorInfo] для хранения описаний операторов. Благодаря тому, что все эти структуры данных являются функциональными, удается создавать множество экземпляров GlobalEnv, не расходуя понапрасну время и память.

    Сопоставление с образцом (pattern matching) и варианты (variant)

    Одна из мощных возможностей, позаимствованных Nemerle у ML-подобных языков – сопоставление с образцом. В Nemerle она выражается в виде оператора match, а также неявного сопоставления с образцом внутри методов и оператора foreach.

    В двух словах, сопоставление с образцом можно описать как switch (из C) на стероидах. Но это будет слишком примитивным взглядом на столь замечательную возможность.

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

    Как следует из названия, операция сопоставления с образцом производит сопоставление некоторого значения с неким образцом и дает ответ на вопрос, соответствует ли объект этому образцу. Ответ выражается в выполнении действия, которое следует непосредственно за стрелкой – «=>».

    Главное преимущество сопоставления с образцом – его декларативность. Фактически, if-ы содержат не указание, что мы хотим найти, а условия и выражения, говорящие о том, как осуществить этот поиск. К тому же, более-менее сложный поиск нужной информации редко умещается в пределах одного if или switch. Образец же является именно выражением того, что нам требуется.

    В статье Nemerle, опубликованной в #1 за 2006 год, подробно рассказано о том, какие виды сопоставления с образцом поддерживаются в Nemerle. В этой же статье речь пойдет о сопоставлении с образцом вариантов.

    Варианты специально разрабатывались таким образом, чтобы их было удобно использовать в операции сопоставление с образцом.

    Чтобы продемонстрировать мощь сочетания сопоставления с образцом и вариантов, я написал простенький пример интерпретатора арифметических выражений, принимающего на вход выражение в виде AST (абстрактного синтаксического дерева) и позволяющего вычислить выражение или преобразовать его в строку. При этом я реализовал два варианта этого интерпретатора. Один – на Nemerle с использованием вариантов и сопоставления с образцом, а второй – на C# в полном соответствии с принципами ООП и применением паттерна проектирования Посетитель для вынесения операций обработки AST из AST-классов.

    Думаю, вряд ли кого-то придется уговаривать, что Nemerle-вариант значительно более краток и, тем не менее, ясен. Причем возьму на себя смелость утверждать, что ясным он будет даже для тех, кто плохо знаком с Nemerle. Так в чем же секрет?

    Мне кажется, основной секрет – в декларативности, обеспечиваемой сопоставлением с образом и сплайс-строками (строками, допускающими вычисляемые выражения, помеченные знаком «$»). По сути, образцы очень близки к BNF-нотации (нотации для описания формальных грамматик языков программирования Бэкуса/Наура).

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

    Варианты

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

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

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

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

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

    ПРИМЕЧАНИЕ

    Вхождение match – это набор паттернов, заканчивающийся знаком «=>».

    Паттерн – это конструкция, начинающаяся со знака «|», за которым идет выражение паттерна и необязательная защита (when ).

    ПРИМЕЧАНИЕ

    В этом примере «| X(Y())» и «X» – это образцы, а вся строка целиком – это вхождение оператора match.

    Лучше всего объяснить все это на примере. Взгляните на код метода ToString() приведенный выше. Там есть следующее вхождение:

    Код $»$e1 $op $e2″ будет выполняться, если на вход выражения match будет подан Expr.Plus, Expr.Minus, Expr.Mul или Expr.Div.

    Тут следует кое-что пояснить. Все эти элементы варианта Expr имеют одинаковые (одинакового типа и смысла) поля – «expr1» и «expr2» (типа Expr). Так как компилятор Nemerle видит, что переменные, введенные в паттернах одного match-вхождения, совпадают по именам, он порождает код, связывающий с этими переменными значения полей того элемента варианта, с которым произошло совпадение образца. Это позволяет писать полиморфный код (работающий вне зависимости от того, с элементом какого конкретного типа произошло сопоставление). Если у одного из вариантов (или даже у всех) нет подходящего поля, то его можно заменить искусственно введенной переменной. Это делается с помощью конструкции with. В данном примере с помощью конструкции with вводится переменная «op», с которой связывается строковый литерал, соответствующий конкретному оператору.

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


    Давайте мысленно протрассируем приведенный выше участок кода. Если на вход приходит значение Expr.Plus(Literal(1), Literal(2)), будет произведено сопоставление с образцом:

    При этом с переменная «e1» будет связана с литералом «1», а «e2» с литералом «2». Их преобразование в строку даст «1» и «2», соответственно. С переменной «op» будет связано значение «+».

    Корреляция между именами переменных, объявляемых в паттерне, и именами полей отсутствует. Так что можно в одном образце одного и того же match-вхождения присвоить имени «e1» значение из поля «expr1», а в другом – из поля «expr2»:

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

    В данном примере «_» используется, чтобы оповестить компилятор о том, что значение первого поля Expr.Minus следует проигнорировать.

    Самым интересным в сопоставлении с образцом является, пожалуй, возможность создавать сложные образцы. Примеры таких образцов можно найти все в том же примере калькулятора:

    Заметьте, что здесь производится не только сопоставление реального типа варианта с типом, указанным в образце, но и производится проверка значений полей элемента варианта. Сначала проверяется, что поле «name» у Expr.Call равно строковому литералу («max» в данном случае), а потом проверяется, что второе поле, parms, содержит список, состоящий из двух элементов (для сопоставления со списками используется синтаксис конструирования списков, который отличается от синтаксиса конструирования обычных вариантов). Причем, поскольку образец списка содержит переменные, они связываются с элементами списка. Таким образом, в приведенном примере переменная arg1 связывается с первым элементом списка, а arg2 – со вторым. В итоге мы получаем декларативную проверку, гарантирующую, что мы имеем дело с вызовом функции по имени «max», имеющей два параметра. И все эти проверки укладываются в 26 символов, включая объявление переменных! Это в несколько раз меньше, чем описание на естественном языке, данное мной :).

    В реальных программах образцы могут быть еще сложнее. Умелое использование вариантов и сопоставления с образцом дает программисту воистину непревзойденную мощь!

    Есть еще много тонкостей связывания. Например, можно связывать переменные образца и поля вариантов явно – по имени, но все это частные тонкости, которые легко почерпнуть из документации на сайте языка, или из вводной статьи на нашем сайте – www.rsdn.ru. Главное, что я пытаюсь донести – это то, как нужно мыслить, чтобы использовать варианты эффективно. Посему, стоит перечислить, чего следует избегать при их использовании. Вот список того, что не следует делать:

    1. Не следует думать о вариантах как о классах. Напротив, о них лучше думать как о простых пассивных типах данных, предназначенных в основном для обработки с помощью сопоставления с образцом.
    2. Не следует выстраивать мысленные иерархии, думая об элементах варианта. Вместо этого каждый элемент варианта надо рассматривать как описание одной сущности.
    3. Не следует бояться дублирования полей в разных элементах варианта. Всегда надо помнить, что обработка общих данных может быть обеспечена за счет возможностей сопоставления с образцом.
    4. Не следует пытаться запихнуть все данные в один элемент варианта. Большое количество полей делает сопоставление с образцом менее удобным и громоздким. Меж тем сами поля элемента варианта могут быть вариантом. Это могут быть другие варианты, или рекурсивная ссылка на этот же вариант. Рекурсивные ссылки позволяют организовывать древесные структуры и списки.
    5. Следует реже использовать в вариантах изменяемые поля. Они допустимы, и иногда даже необходимы (например, для создания графов, или если часть информации становится доступна значительно позже того момента, когда требуется создать экземпляр элемента варианта), но в основном их применение обычно необосновано и происходит из-за того, что программист просто пока не привык использовать неизменяемые структуры данных. Хорошим шагом в этом направлении будет думать о вариантах как о монолитных значениях вроде int или DateTime.
    6. Не стоит без особых на то причин использовать в вариантах коллекции, отличные от list[T]. Дело в том, что list[T] сам по себе является вариантом, что позволяет использовать его в конструкциях сопоставления с образцом. Причем это позволяет создавать сложные образцы, включающие информацию о корневом варианте, списке, и даже о содержимом отдельных элементов списка. Кроме того, преимуществом list[T] является то, что он является неизменяемым типом, а значит, хорошо вписывается в другие неизменяемые типы.

    В отличие от классических алгебраических типов, варианты допускают наличие методов. Причем методы могут быть даже виртуальными. Это позволяет наряду с утиным полиморфизмом match-а использовать и полиморфизм в стиле ООП. Единственно, надо понимать, что этот полиморфизм ограничивается двумя уровнями (вариантом и его элементами) и позволяет выстраивать иерархии как в случае с классами.

    Если при использовании вариантов у вас все время возникает желание увидеть иерархию наследования, то стоит задуматься, не стоит ли заменить варианты классами.

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

    Не match-ем единым.

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

    Оператор «is»

    Если вам не требуется извлекать данные из паттерна или использовать множество match-вхождений с множеством образцов, а требуется всего лишь узнать, соответствует ли некоторое значение некоторому образцу, то можно воспользоваться оператором «is». Его синтаксис прост:

    Результатом этого выражения является булево значение. Если выражение соответствует паттерну – true и наоборот. Оператор «is» удобно использовать в операторах if/when и других контекстах, где требуется булево значение.

    Анонимный match

    Если тело функции состоит из выражения match, в которое передаются все параметры этой функции (или метода), например:

    то выражение match можно опустить, расположив match-вхождения непосредственно в теле функции:

    При этом можно опустить и само имя параметра, заменив его подстановочным знаком «_».

    Сопоставление с образцом в параметрах локальных функций

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

    Можно применять и литеральные паттерны (например, заменить переменную b значением «1»), но при этом надо понимать, что из-за отсутствия альтернативного match-вхождения в случае неудачного сопоставления с образцом будет сгенерировано исключение. Что, на мой взгляд, очень плохо.

    foreach

    Очень удобно использовать сопоставление с образцом в операторе foreach. При этом доступны сразу несколько возможностей.

    1. Вы можете отфильтровать нужные элементы по типу. Например, предположим, что вам требуется из общего списка членов (typeMembers) некоторого типа отфильтровать только те элементы, которые реализуют интерфейс IMethod (то есть являются методами). Это можно сделать следующим образом:

    2. Вы можете использовать защиту, чтобы обработать только нужные элементы. Например, вот так можно исключить из обработки элементы, значения которых равны null:

    Другой пример – вы можете захотеть обработать только публичные методы:

    В принципе, этот прием аналогичен использованию отдельного оператора when, но зачастую бывает более лаконичен.

    3. Вы можете указывать один образец варианта. При этом будут отфильтрованы только те элементы, которые соответствуют этому образцу:

    3. Вы можете производить декомпозицию вариантов и кортежей непосредственно при объявлении элемента в foreach:

    4. Самый сложный случай практически аналогичен анонимному применению match в функциях:

    Заключение

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

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

    Что значит «Строки в C# неизменяемы»?

    Пытаюсь понять что значит «строки не «изменяемы»? И казалось бы в этой статье автор из кожи вон лез и все по полочкам расставил, но я не понял. И для начала хотел бы понять как примерно выглядит память на разных этапах выполнения ниже представленного кода:

    1) И так, после выполнения

    2) Далее, после выполнения

    память выглядит так:

    Т.е. создается ссылка str2 которая указывает на новый участок памяти и туда копируется строка из str1 .

    Я правильно описал этот код?

    3 ответа 3

    Нет. string str2 = str1 здесь вы копируете ссылку в новую переменную. А здесь str2 = str1 + «+2» вы создали новый объект. str1 продолжает ссылаться на старый.

    Можно проверить равенство ссылок, с помощью ReferenceEquals

    Строки «неизменяемы» это значит, что несмотря на то, что это объекты ссылочного класса, они имеют признак типов-значений — неизменяемость. Создав один раз объект string вы просто не сможете его изменить, нет ни одного метода для этого.

    Даже так str2[0] = ‘!’ вы не сможете сделать, компилятор не даст вам изменить объект строки таким способом. А такие методы как Replace, Substring, Trim возвращают новый объект.

    Я думаю, проще всего вам будет представить String как что-то типа структуры. Да, он может принимать null , и в этом его особенность. Но он не предназначен для изменения. Это сделано из соображений безопасности и удобности. Если хотите изменять строку str[i] = ‘b’ , то вам нужен StringBuilder . Вот его-то как раз и можно изменять так, как вы писали. Кстати, в нем многие методы похожи на методы в String .

    Отличие: Например, при String.Replace(..) сама строка, метод которой вы вызываете, не изменится(Зато возвратится новый String , который будет уже таким, каким вы хотите). В свою очередь, при StringBuilder.Replace(..) также возвращается новая строка, отвечающая вашим требованиям. Но при этом сам объект StringBuilder также изменится. Сейчас приведу пример кода.

    В строке str2=str1: если структура struc будет передано значение новой переменной str2, а в классе class — ссылка.

    Всё ещё ищете ответ? Посмотрите другие вопросы с метками c# .net строки память clr или задайте свой вопрос.

    Связанные

    Похожие

    Подписаться на ленту

    Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

    дизайн сайта / логотип © 2020 Stack Exchange Inc; пользовательское содержимое попадает под действие лицензии cc by-sa 4.0 с указанием ссылки на источник. rev 2020.11.9.35389

    Справочник современных концепций JavaScript: часть 1

    Основы функционального программирования, реактивного программирования и функционального реактивного программирования на JavaScript

    Кратко: В первой части серии «Справочник современных концепций JavaScript» мы познакомимся с функциональным программированием (FP), реактивным программированием (RP) и функциональным реактивным программированием (FRP). Для этого мы узнаем о чистоте, состоянии и его отсутствии, неизменяемости и изменяемости, императивном и декларативном программировании, функциях высшего порядка, observables и парадигмах FP, RP, FRP.

    Введение

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

    Концепции

    В этой статье мы рассмотрим концепции, имеющие решающее значение для понимания функционального программирования, реактивного программирования и функционального реактивного программирования и их использования с JavaScript:

    • Чистота: чистые функции, нечистые функции, побочные эффекты
    • Состояние: с ним и без него
    • Неизменяемость и изменяемость
    • Императивное и декларативное программирование
    • Функции высшего порядка
    • Функциональное программирование
    • Observables: горячие и холодные
    • Реактивное программирование
    • Функциональное реактивное программирование

    Чистота

    Чистые функции

    Возвращаемое значение чистой функции зависит только от ее входных данных (аргументов) и не влечет никаких побочных эффектов. С одним и тем же входящим аргументом результат всегда будет одинаковый. Пример:

    Функция half(x) принимает число x и возвращает значение половины x . Если мы передадим этой функции аргумент 8 , она вернет 4 . После вызова чистая функция всегда может быть заменена результатом своей работы. Например, мы могли бы заменить half(8) на 4 : где бы эта функция не использовалась в нашем коде, подмена никак не повлияла бы на конечный результат. Это называется ссылочной прозрачностью.

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

    • Чистые функции должны принимать аргументы.
    • Одни и те же входные данные (аргументы) всегда произведут одинаковые выходные данные (вернут одинаковый результат).
    • Чистые функции основываются только на внутреннем состоянии и не изменяют внешнее ( примечание: console.log изменяет глобальное состояние).
    • Чистые функции не производят побочных эффектов.
    • Чистые функции не могут вызывать нечистые функции.

    Нечистые функции

    Нечистая функция изменяет состояние вне своей области видимости. Любые функции с побочными эффектами (см. далее) — нечистые, ровно как и процедурные функции без возвращаемого значения.

    Рассмотрим следующие примеры:


    Побочные эффекты в JavaScript

    Когда функция или выражение изменяет состояние вне своего контекста, результат является побочным эффектом. Примеры побочных эффектов: вызов API, манипулирование DOM, вывод alert, запись в базу данных и так далее. Если функция производит побочные эффекты, она считается нечистой. Функции, вызывающие побочные эффекты, менее предсказуемы и их труднее тестировать, поскольку они приводят к изменениям вне их локальной области видимости.

    Подводя итог: чистота

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

    Дополнительную информацию о чистоте можно найти на следующих ресурсах:

    Состояние

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

    С состоянием

    Программы, приложения или компоненты с состоянием хранят в памяти данные о текущем состоянии. Они могут изменять состояние, а также имеют доступ к его истории. Следующий пример демонстрирует это:

    Без состояния

    Функции или компоненты без состояния выполняют задачи, словно каждый раз их запускают впервые. Они не ссылаются или не используют в своем исполнении раннее созданные данные. Отсутствие состояния обеспечивает ссылочную прозрачность. Функции зависят только от их аргументов и не имеют доступа, не нуждаются в знании чего-либо вне их области видимости. Чистые функции не имеют состояния. Пример:

    Приложения без состояния все еще управляют состоянием. Однако они возвращают свое текущее состояние без изменения предыдущего состояния. Это принцип функционального программирования.

    Подводя итог: состояние

    Управление состоянием важно для любого сложного приложения. Функции или компоненты с состоянием изменяют состояние и его историю, их труднее тестировать и отлаживать. Функции без состояния полагаются только на свои входные данные для создания данных выходных. Программа без состояния возвращает новое состояние, а не модифицирует существующее состояние.

    Дополнительную информацию о состоянии можно найти на следующих ресурсах:

    Неизменяемость и изменяемость

    Концепции неизменяемости и изменяемости более туманны в JavaScript, чем в некоторых других языках программирования. Тем не менее, вы много услышите о неизменяемости при чтении о функциональном программировании в JS. Важно знать, что эти термины означают в классическом понимании, и как они реализуются в JavaScript. Определения достаточно просты:

    Неизменяемый

    Если объект является неизменяемым, его значение не может быть изменено после создания.

    Изменяемый

    Если объект изменяем, его значение может быть изменено после создания.

    Реализация: неизменяемость и изменяемость в JavaScript

    В JavaScript строки и числовые литералы реализованы неизменяемыми. Это легко понять, если рассмотреть, как мы работаем с ними:

    Используя метод .substring() на нашем Hello! , строка не изменяет исходную строку. Вместо этого она создает новую строку. Мы могли бы переопределить значение переменной str на что-то другое, но, как только мы создали нашу строку Hello! , она навсегда останется Hello! .

    Числовые литералы также неизменяемы. Следующий пример всегда будет иметь одинаковый результат:

    Ни при каких обстоятельствах 1 + 2 не может стать чем-либо, кроме 3 .

    Это демонстрирует, что в JavaScript присутствует реализация неизменяемости. Однако разработчики JS знают, что язык позволяет изменить многое. Например, объекты и массивы изменяемы. Рассмотрим следующий пример:

    В этих примерах исходные объекты изменены. Новые объекты не возвращаются.

    Чтобы узнать больше об изменяемости в других языках, ознакомьтесь с Изменяемые и неизменяемые объекты.

    На практике: изменяемость в JavaScript

    Функциональное программирование в JavaScript хорошо развивается. Но по своей сущности JS — очень изменчивый язык, состоящий из множества парадигм. Ключевая особенность функционального программирования — неизменяемость. Другие функциональные языки выбросят ошибку, когда разработчик попытается изменить неизменяемый объект. Тогда как мы можем примирить врожденную изменяемость JS при написании функционального или функционального реактивного JS?

    Когда мы говорим о функциональном программировании в JS, слово «неизменяемое» используется много, но разработчик обязан всегда держать ее в голове. Например, Redux полагается на одно неизменяемое дерево состояний. Однако сам JavaScript способен изменять объект состояния. Чтобы реализовать неизменяемое дерево состояний, нам нужно каждый раз при изменении состояния возвращать новый объект состояния.

    Для неизменяемости объекты JavaScript также могут быть заморожены с помощью Object.freeze(obj) . Обратите внимание, что это «неглубокая» заморозка — значения объектов внутри замороженного объекта все еще могут быть изменены. Для гарантированной неизменяемости такие функции «глубокой» заморозки, как Mozilla deepFreeze() и npm deep-freeze могут рекурсивно замораживать объекты. Замораживание наиболее применимо в тестах, а не в приложении. Тесты будут оповещать разработчиков о возникновении изменений, чтобы их можно было исправить, и избежать загромождающего Object.freeze в основном коде.

    Существуют также библиотеки, поддерживающие неизменяемость в JS. Mori предоставляет постоянные структуры данных на основе Clojure. Immutable.js от Facebook также предоставляет неизменяемые коллекции для JS. Библиотеки утилит, такие как Underscore.js и lodash, предоставляют методы и модули для более функционального стиля программирования (а стало быть направленного на неизменяемость).

    Подводя итог: неизменяемость и изменяемость

    В целом, JavaScript — язык с сильной изменяемостью. Некоторые стили JS-кодирования опираются на эту врожденную изменяемость. Однако, при написании функционального JS, реализация неизменяемости требует внимательности. Если вы что-то нечаянно модифицируете, JS не будет выбрасывать ошибки. Тестирование и библиотеки могут помочь, но работа с неизменяемостью в JS требует практики и методологии.

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

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

    Дополнительную информацию о неизменяемости и изменяемости можно найти на следующих ресурсах:

    Императивное и декларативное программирование

    Хотя некоторые языки были разработаны как императивные (C, PHP) или декларативные (SQL, HTML), JavaScript и другие (такие как Java и C# могут поддерживать обе парадигмы программирования.

    Большинство разработчиков, знакомых с даже самым простым JavaScript, писали императивный код: инструкции, информирующие компьютер как достичь желаемого результата. Если вы использовали цикл for , вы писали императивный JS.

    Декларативный код сообщает компьютеру, что вы хотите достичь, а не как, и компьютер сам заботится о том, как достичь конечного результата без явного описания этого разработчиком. Если вы использовали Array.map , вы писали декларативный JS.

    Императивное программирование

    Императивное программирование описывает логику работы программы в явных командах с операторами, изменяющими состояние программы.

    Рассмотрим функцию, увеличивающую каждое число в массиве целых чисел. Императивным примером в JavaScript может быть:

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

    Декларативное программирование

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

    Очень простой пример декларативного программирования может быть продемонстрирован с помощью SQL. Мы можем запросить таблицу базы данных ( People ) для людей с фамилией Smith следующим образом:

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

    Теперь рассмотрим функцию incrementArray() , которую мы императивно реализовали выше. Давайте сейчас реализуем это декларативно:

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

    Примечание: в JavaScript map, reduce и filter — декларативные, функциональные методы массивов. Библиотеки утилит, такие как lodash, предоставляют такие методы takeWhile, uniq, zip и другие в дополнение к map , reduce и filter .

    Подводя итог: императивное и декларативное программирование

    JavaScript допускает как парадигмы императивного, так и декларативного программирования. Большая часть кода JS, который мы читаем и пишем, императивная. Однако, с ростом популярности функционального программирования в JS, декларативные подходы распространяются все больше.

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

    Дополнительную информацию об императивном и декларативном программировании можно найти на следующих ресурсах:

    Функции высшего порядка

    Функция высшего порядка — это функция, которая принимает другую функцию в качестве аргумента или возвращает функцию в результате.

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

    Обратный вызов — один из примеров использования функции в качестве аргумента. Обратные вызовы могут быть встроенными анонимными функциями или именованными функциями:

    Мы также можем передать функцию в качестве аргумента любой другой созданной нами функции, а затем выполнить этот аргумент:

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

    Функции высшего порядка также могут возвращать другую функцию:

    Подводя итог: функции высшего порядка

    Природа JavaScript функций как объектов первого класса делает их основой функционального программирования в JS.

    Дополнительную информацию о функциях высшего порядка можно найти на следующих ресурсах:

    Функциональное программирование

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

    На практике: функциональное программирование на JavaScript

    Функциональное программирование охватывает приведенные выше концепции следующими способами:

    • Основные функции реализованы с использованием чистых функций без побочных эффектов.
    • Данные неизменяемы.
    • Функциональные программы не имеют состояния.
    • Императивный код контейнера управляет побочными эффектами и выполняет декларативный, чистый код ядра.

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

    Давайте рассмотрим пример. Скажем, у нас есть текст, и мы хотим посчитать количество слов в нем. Мы также хотим найти ключевые слова длиной более пяти символов. Используя функциональное программирование, наш результирующий код может выглядеть примерно так:

    Код доступен для запуска здесь — JSF >processCopy() — нечистый контейнер, выполняющий ядро ​​и управляющий побочными эффектами. Мы использовали функцию высшего порядка, принимающую другие функции в качестве аргументов для поддержания функционального стиля.

    Подводя итог: функциональное программирование

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


    Дополнительную информацию о функциональном программировании можно найти на следующих ресурсах:

    Observables

    Observables похожи на массивы, за исключением того, что элементы не хранятся в памяти, а поступают асинхронно с течением времени (потоки). Мы можем подписаться на observables и реагировать на их события. JavaScript оbservables — это реализация шаблона observer. Реактивные расширения (обычно называемые с префиксом Rx) предоставляют observables для JS через RxJS.

    Для демонстрации концепции observables давайте рассмотрим простой пример: изменение размера окна браузера. В этом контексте observables максимально понятны. Изменение размера окна браузера испускает поток событий в течение определенного периода времени (пока окно принимает нужный размер). Мы можем создать observable и подписаться на него, чтобы реагировать на поток событий изменения размера:

    Пример кода выше демонстрирует, что, по мере изменения размера окна, мы можем ограничивать observable поток и подписаться на его изменения, чтобы реагировать на новые значения в коллекции. Это пример горячего observable.

    Горячие observable

    Пользовательские интерфейсные события (нажатие кнопки, перемещения мыши и т.д.) — горячие. Горячие observable всегда будут срабатывать, даже если мы специально не реагируем на них подпиской. Пример изменения размера окна выше — горячий observable: resize$ observable отрабатывает вне зависимости от того, существует подписка или нет.

    Холодные observable

    Холодные observable срабатывают только тогда, когда мы подписываемся на него. Если мы подпишимся снова, он начнет все сначала.

    Давайте создадим observable массив чисел от 1 до 5 :

    Мы можем подписаться на только что созданный source$ observable. После подписки, значения последовательно посылаются к observer. Обратный вызов onNext логирует значения: Next: 1 , Next: 2 , и т.д. до завершения: Completed! . Созданный нами холодный observable source$ не отрабатывает до того, пока мы не подпишемся на него.

    Подводя итог: observables

    Observables являются потоками. Мы можем наблюдать любой поток: от событий изменения размеров в существующих массивах до ответов API. Мы можем создать observables практически из всего, что угодно. Promise — тот же observable, но отдающий только одно значение, а observables могут возвращать много значений с течением времени.

    Мы можем работать с observables по-разному. RxJS использует множество операторов. Observables часто визуализируется с помощью точек на линии, как показано на сайте RxMarbles. Поскольку поток состоит из асинхронных событий с течением времени, легко осмыслять это линейным способом и использовать именно такие зрительные образы, чтобы понять реактивные операторы. Например, следующие изображение от RxMarbles иллюстрирует оператор фильтра:

    Дополнительную информацию об observables можно найти на следующих ресурсах:

    Реактивное программирование

    Реактивное программирование связано с декларативным (что делать, а не как) наблюдением и реагированием на поступающие события во времени.

    Реактивное программирование часто связано с Reactive Extensions, API для асинхронного программирования с observable потоками. Реактивные расширения (с префиксом Rx) предоставляют библиотеки для различных языков, включая JavaScript (RxJS).

    На практике: реактивное программирование на JavaScript

    Вот пример реактивного программирования с observables. Предположим, у нас есть вход, где пользователь может ввести шестизначный код подтверждения, и мы хотим вывести его, когда будет введено последнее число. Наш HTML может выглядеть так:

    Мы будем использовать RxJS и для реализации нашей функциональности создадим поток входных событий, вот так:

    Этот код можно запустить на JSF >confCodeInput . Затем мы используем оператор map для получения значения из каждого входного события. Далее мы фильтруем любые результаты, не являющиеся шестью символами, чтобы они не появлялись в возвращенном потоке. Наконец, мы подписываемся на наш confCodes$ observable и выводим подошедший вариант. Обратите внимание, что это произошло в ответ на событие, декларативно, — это суть реактивного программирования.

    Подводя итог: реактивное программирование

    Парадигма реактивного программирования включает наблюдение и реагирование на события в асинхронных потоках данных. RxJS используется в Angular и набирает популярность как решение JavaScript для реактивного программирования.

    Дополнительную информацию о реактивном программировании можно найти на следующих ресурсах:

    Функциональное реактивное программирование

    Говоря простыми словами, функциональное реактивное программирование можно свести к декларативному реагированию на события или поведение с течением времени. Чтобы понять принципы FRP более подробно, давайте взглянем на формулировку FRP. Затем мы рассмотрим его использование применительно к JavaScript.

    Что такое функциональное реактивное программирование?

    Более полное определение от родоначальника FRP Конала Эллиота звучит так: функциональное реактивное программирование — денотативное и продолжительное во времени. Эллиот предпочитает описывать эту парадигму программирования как денотативное программирование с продолжительностью во времени, а не «функциональное реактивное программирование».

    Функциональное реактивное программирование, в его самом простом, оригинальном определении, имеет два фундаментальных свойства:

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

    Чтобы понять продолжительность во времени, рассмотрим аналогию с использованием векторной графики. Векторная графика имеет бесконечное разрешение. В отличие от растровой графики (дискретное разрешение) векторная графика масштабируется безгранично, она никогда не перерисовывается или становится нечеткой. «Выражения FRP описывают целые эволюции значений во времени, представляя эти эволюции непосредственно как значения первого класса», — Конал Эллиот.

    Функциональное реактивное программирование должно быть:

    • динамическим: может реагировать во времени или на изменение входных данных
    • изменяющимся во времени: реактивное поведение может изменяться с продолжительностью во времени, а реактивные значения изменяются дискретно
    • эффективным: минимизировать объем обработки, необходимый при изменении входных данных
    • осведомленным о прошлом: чистые функции сопоставляют предыдущее значение состояния со следующим; изменения состояния относятся к локальному элементу, а не к глобальному состоянию программы

    Здесь можно рассмотреть слайды Колала Эллиота о cущности и происхождении FRP. Язык программирования Haskell поддается истинному FRP благодаря своей функциональной, чистой и ленивой природе. Эван Кзаплиски, создатель Elm, дает большой обзор FRP в своем выступлении «Управление временем и пространством: понимание многих подходов FRP».

    В самом деле, давайте коротко поговорим об Elm Эвана Кзаплиски. Elm — это функциональный, типизированный язык для создания веб-приложений. Он компилируется в JavaScript, CSS и HTML. Архитектура Elm послужила вдохновением для контейнера состояния Redux приложений JS. Первоначально Elm позиционировался истинным функционально реактивным языком программирования, но начиная с версии 0.17 он реализовывал подписки вместо сигналов в интересах облегчения изучения и использования языка. На этом Elm простился с FRP.

    На практике: функциональное реактивное программирование на JavaScript

    Традиционное определение FRP может быть трудным для понимания, особенно для разработчиков, не имеющих опыта работы с такими языками, как Haskell или Elm. Однако этот термин чаще всего появляется в интерфейсной экосистеме, поэтому давайте проясним его применение в JavaScript.

    Для согласования всего, что вы, возможно, читали о FRP в JS, важно понять, что Rx, Bacon.js, Angular и другие не согласуются с двумя основными принципами определения FRP Конала Эллиота. Эллиот заявляет, что Rx и Bacon.js не являются FRP. Вместо этого они «композиционные системы событий, вдохновленные FRP».

    Функциональное реактивное программирование в своей реализации в JavaScript относится к программированию в функциональном стиле при создании и реагировании на потоки. Это довольно далеко от оригинальной формулировки Эллиота (которая специально исключает потоки как компонент), но тем не менее вдохновляется традиционными FRP.

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

    Давайте взглянем на пример, чтобы продемонстрировать основные принципы FRP-вдохновленного JavaScript. Этот пример использует RxJS и печатает движения мыши в течение десяти секунд:

    Вы можете проверить этот код в действии в JSFiddle: FRP-вдохновленном JavaScript. Запустите скрипт и, пока идет подсчет до 10, наведите указатель мыши в экран с результатом. Вы должны увидеть координаты мыши вместе со счетчиком. Тогда на экран выведется, где была ваша мышь во время каждого 1-секундного интервала времени.

    Давайте кратко обсудим эту реализацию шаг за шагом.

    Сначала мы создаем observable time$ . Это таймер, добавляющий значение в коллекцию каждые 1000 миллисекунд (каждую секунду). Нам нужно обработать событие таймера, чтобы извлечь его значение и добавить к результирующему потоку.

    Затем мы создаем move$ observable из события document.mousemove . Движение мыши продолжительно во времени. В любой точке последовательности существует бесконечное количество точек между ними. Мы ограничиваем эту бесконечность, так что результирующий поток становится более управляемым. Затем мы обрабатываем событие и возвращаем объект с значениями x и y , чтобы сохранить координаты мыши.

    Затем мы хотим объединить потоки time$ и move$ . Для этого используем оператор объединения. Так мы можем определить, какие движения мыши произошли в течение каждого интервала времени. Мы будем называть результирующий observable source$ . Мы также ограничим observable source$ так, чтобы он завершился через десять секунд ( 10000 миллисекунд ).

    Теперь, когда у нас есть объединенный поток времени и движения, мы создаем подписку на observable source$ , чтобы мы могли реагировать на него. В обратном вызове onNext мы проверяем, является ли значение числом или нет. Если это так, мы вызываем функцию createTimeset() . Если это объект координат, вызываем addPoint() . В обратных вызовах onError и onCompleted мы просто логируем информацию.

    Давайте рассмотрим функцию createTimeset(n) . Мы создаем новый элемент div для каждого второго интервала, помечаем его и добавляем в DOM.

    В функции addPoint(pointObj) мы выводим в div последние координаты в последнем временном интервале. Это связывает каждый набор координат с соответствующим временным интервалом. Теперь мы можем увидеть, где мышь находилась в конкретный момент времени.

    Примечание: эти функции нечистые: у них нет возвращаемого значения и они производят побочные эффекты. Побочные эффекты — манипуляции DOM. Как упоминалось ранее, JavaScript, который мы пишем для наших приложений, часто взаимодействует с областью видимости вне его функций.

    Подводя итог: функциональное реактивное программирование

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

    Наконец, рассмотрим эту цитату из первого издания Eloquent JavaScript: «Fu-Tzu написал небольшую программу, использующую глобальное состояние и сомнительные переплетения, и, прочитав ее, студент спросил:« Вы предупреждали нас против этих методов, но я нахожу их в вашей программе. Как такое могло случиться?». Фу-Цзы ответил: «Нет необходимости забирать водяной шланг, когда дом не горит». Это не следует рассматривать как поощрение неаккуратного программирования, а скорее как предупреждение против невротического соблюдения эмпирических правил .

    Дополнительную информацию о функциональном реактивном программировании можно найти на следующих ресурсах:

    Заключение

    Мы закончим еще одной отличной цитатой из первого издания Eloquent JavaScript: «Студент долгое время сидел за своим компьютером, мрачно хмурился и пытался написать красивое решение сложной проблемы, но не мог найти правильный подход. Фу-Цу ударил его по затылку и крикнул: ‘Введите что-нибудь!’. Студент начал писать уродливое решение, и после того, как он закончил, он внезапно понял прекрасное решение».

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

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

    Читайте нас на Медиуме , контрибьютьте на Гитхабе , общайтесь в группе Телеграма , следите в Твиттере и канале Телеграма , скоро подъедет подкаст. Не теряйтесь.

    Объекты на C#

    Опубликовано Константин Туйков в 20.06.2020 20.06.2020

    Язык C# входит в число объектно-ориентированных языков программирования. Это значит, что декомпозиция реально существующих систем производится на объектном уровне. То есть выделяются объекты системы, их составные части и свойства.

    Классы

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

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

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

    Итак, мы имеем класс некоторого персонажа, который пока не умеет делать совершенно ничего. Обучать его ходить, атаковать и защищаться мы будем после, когда начнем изучать методы в C#.

    Чтобы создать несколько таких персонажей нам достаточно написать:

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

    Модификаторы доступа

    Как вы могли заметить, в примере было указано слово public, являющееся так называемым модификатором доступа. Всего существует четыре модификатора:

    • public — свойство, метод или класс доступен во всем проекте;
    • protected — свойство или метод доступны объекту и его потомкам (наследование будет рассмотрено позже);
    • private — свойство или метод доступны только объекту;
    • internal — свойство или метод доступны в пределах сборки (assembly);
    • protected internal — доступ в пределах сборки, а так же всем типам от данного.

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

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


    Также рекомендую изучить статью C# константы и readonly.

    Дизайн и эволюция свойств в C# (часть 1)

    08.04.2015 • Шведов Александр

    Идея поста родилась из обычной рабочей задачи — поддержать нововведения языка C# 6.0 в ReSharper. Как обычно, все задачи в ReSharper оказываются в 5-10 раз сложнее, чем кажется на первый взгляд. Особой головной болью оказалась поддержка новых возможностей свойств в C# 6.0, так как изменения языка затрагивали массу кода имеющейся функциональности (далеко не всегда очевидным образом). Переделки заняли несколько месяцев, иногда вынуждая переписывать некоторые рефакторинги практически целиком (конкретно — “Convert property to auto-property”), что заставило меня задуматься — почему все настолько сложно в мире свойств C#? Как так получилось, что работая со свойствами C# в IDE-инструментарием, надо постоянно держать в голове просто массу знаний о них? Чувствуют ли эту “сложность” обычные программисты?

    Итак, сегодня я предлагаю вам в деталях обсудить понятие “свойства” на примере языка C# с самой первой его версии, немного поразмышлять о дизайне языков программирования, о том куда этот дизайн может катится и о том, можно ли хоть что-нибудь исправить.

    Ликбез по свойствам

    Что это вообще такое?

    Давайте вернемся во времена C# 1.0 и рассмотрим определение канонического DTO-класса, инкапсулирующего некоторые данные. В отличие от Java, объявления класса в C# могла содержать не только поля/методы/классы, но и еще одну разновидность членов класса — объявления свойств:

    Свойства представляю собой члены класса, обладающие именем и типом, а так же содержащие в себе объявления “аксессоров”. Акцессоры немного похожи на объявления методов, за исключением, например, явной указания типа возвращаемого значения и списка формальных параметров. Существуют два вида аксессоров свойств — getter’ы и setter’ы, вызываемых при обращении к свойству на чтение и запись соответственно. Возможно, на тот момент это казалось языковым средством неземной красоты, по сравнению со шляпой из пары методов в Java (которую шлепают до сих пор, в 2015 году):

    Мотивация

    ОК, зачем нам вообще нужны свойства? Дело в том, что в высокоуровневых языках, таких как в Java и C#, поля классов представляют собой достаточно низкоуровневые конструкции 1 . Доступ к полю просто считывает или записывает область памяти некоторого известного размера по некоторому смещению, статически известному среде исполнения. Из такой низкоуровневости полей следует, что:

    Между разными полями и полями разных типов не существует унифицированного “интерфейса” (такого, например, как указатель на исполняемый код), что не позволяет организовать к ним полиморфный доступ (написать код, абстрагированный от знания о конкретном типе, к полю которого он обращается);

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

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

    Все эти проблемы можно было успешно решить существующими в языке средствами — введя пару методов для доступа к значению поля, реализуя ими члены интерфейса или исполняя произвольный код проверок до или после записи:

    Чем неудобно такое решение?

    • Мы потеряли привычный синтаксис доступа к данным в полях:

    В программе на самом деле никак не выражено, что все три сущности — поле и пара методов — имеют какую-либо связь. Методы и поля могут иметь разный уровень видимости, разное имя, разную “статичность” и “виртуальность”.

    Чтобы намекнуть на общую связь, мы объявили три сущности имеющими подстроку “name” в именах. При рефакторинге нам придется обновить все три имени. Аналогично с упоминанием типа данных. Подобное соглашение об именовании упрощает жизнь в Java, но носит лишь рекомендательный характер (компилятор не будет бить по рукам в случае игнорирования соглашений).

    Решение с помощью свойств

    Давайте посмотрим на пример кода выше, переписанный с использованием объявлений свойств C#:

    Кода по-прежнему невыносимо много, но определенные удобства свойства все же привносят:

    Тела аксессоров синтаксически объединены в один блок, а значит логически разделяют один и тот же уровень видимости, модификаторы статичности и виртуальности;

    Подстрока “name” теперь встречается в объявлениях “всего” два раза, так же как и тип String . Параметр value объявляется в set -аксессоре неявным образом, что тоже экономит немного кода;

    Синтаксис доступа к свойствам аналогичен привычному и простому синтаксису доступа к полю, что еще и значительно облегчает инкапсуляцию поля в свойство вручную (без автоматизированных рефакторингов кода);

    За свойством может и вовсе не стоять поля с состоянием, тела аксессоров могут содержать произвольный код;

    Не смотря на то, что аксессоры все равно компилируются в тела методов string get_Name() и set_Name(string value) , в метаданных хранится специальная запись о свойстве, как о единой сущности (аналогично для событий C#). Таким образом понятие “свойства” существует для среды исполнения, это не только сущность компилятора C#. Как следствие, например, свойства можно помечать атрибутами CLI как единую сущность, что имеет множество применений на практике.

    Как я не пытался структурировать дальнейшие рассуждения, дальше получились просто перечислить приемущества и недостатки свойств вообще и дизайна свойств конкретно в языке C# самой первой версии.

    Set-only свойства

    Тут нечего особо обсуждать — в C# никогда не должно было случиться свойств из единственного set -аксессора:

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

    Например, запрет объявления свойств из одного set -аксессора может казаться натуральной “заплаткой” с точки зрения красоты спецификации языка, каким-то искусственным ограничением, разрушающим симметрию между разными типами аксессорами 2 ( get -аксессоры становятся обязательными).

    С другой стороны, если не запрещать такие очевидно странные сущности (на такие set -only свойства в языке похожи только out -параметры, но их становится возможно считывать после первого присвоения), то начинает случаться реальный говнокод с set -only свойствами (я пару раз встречал). Если пару раз наткнуться на такие свойства, не понимая почему их значение не видно в отладчике, то начинаешь ценить совсем не каноничность определения свойства в спецификации, а в количество фашизма запретов в компиляторе.

    Разновидности доступа

    Простой императивный язык программирования — это такой, в коде которого можно увидеть использование (не объявление!) переменной foo и всегда знать, что это либо чтение переменной, либо запись. Но когда-то очень давно случился язык программирования C и теперь у нас есть вот эти мутанты:

    То есть появляется новая разновидность использования — чтение/запись 3 . Так как C# стремится синтаксически устранить различия использования свойств от использования полей, то подобные операторы разрешены и для свойств (доступных для записи), компилируясь в вызовы get и set -аксессоров:

    Казалось бы: замечательно, что это работает — ведь на то и нужен язык высокого уровня, что скрывать низкоуровневые детали реализации свойств как вызовов методов. Проблема в том, что в C# есть еще один источник использований на чтение и запись одновременно — ref -параметры:

    К сожалению, ref / out -параметры в C# — не менее низкоуровневые конструкции, чем поля типов. Для среды исполнения ref / out -параметры имеют специальный управляемый ссылочный тип, отличающегося от обычных unmanaged-указателей только запретом на арифметический операции и осведомленностью GC об объектах по таким указателям (передача поля класса/элемента массива как ref / out -параметра удерживает весь объект/массив от сборки мусора).

    Из-за невозможности превратить два метода аксессоров свойства в один указатель на изменяемую область памяти, компилятор C# банально не позволяет передавать свойства в ref / out -параметры. Это редко нужно на практике, но выглядит как “спонтанное нарушение симметрии” в языке. Интересно, что другой .NET-язык — Visual Basic .NET — без особых проблем скрывает для пользователя разницу между свойствами и полями:

    Грубо говоря, VB.NET разрешает передавать в byref -параметры вообще любые выражения, автоматически создавая временную локальную переменную (и передавая ее адрес). Если в качестве аргумента byref -передано изменяемое свойство, то VB.NET автоматически присвоит свойству значение временной переменной, но только по окончанию вызова. Есть вероятность (крайне маленькая), что метод с byref -параметром каким-нибудь магическим образом должен зависеть от актуального значения переданного в него свойства и тогда удобность превратится в грабли.

    Но граблей и без этого хватает: например, если взять в скобки i.Prop из примера выше, то присвоение свойству перестанет происходить (в качестве byref -аргумента начнет передаваться временное значение выражения в скобках, а не само свойство-как-адрес). Помимо этого, присвоение свойству не случится если после присвоения byref -параметра в методе возникнет исключение. Вот и не понятно становится — стоят-ли эти грабли в языке потерянной универсальности?

    1 В CLR чтение полей MarshalByReference объектов на самом деле всегда виртуальное (до тех пор пока его не передать в ref / out -параметр).

    2 В C# уже существуют другие типы членов класса с аксессорами — события — для которых компилятор всегда требует определить оба ацессора ( add и remove ).

    3 На самом деле я конечно не упоминаю другие типы использований сущностей в C# — использования в XML-документации, использования имен сущностей в операторе nameof() из C# 6.0, “частичные” использования на чтение и запись при работе с типами-значениями:

    Готовимся к собеседованию .Net,C#,WPF,WCF,ODP,PM

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

    пятница, 20 ноября 2009 г.

    50 вопросов по .Net

    1. Поддерживает ли C# interfaces множественное наследование
    да

    2. Поддерживает ли C# class множественное наследование
    нет . Только наследование от одного класса и множества интерфейсов

    3. Какая разница между классами и структурами
    структуа — тип значения, хранится в стеке, не может наследоваться; класс — тип ссылка, хранится в куче.

    4. HashTable — что это такое, как устроен
    Позволяет получить доступ к члену коллекции с использованием уникального ключа.
    System.Collections.Hashtable : IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback, ICloneable

    5. Кому доступна protected переменная класса
    -Доступна любому классу который наследует данный класс

    6. Наследуются ли переменные класса с областью видимости private

    -Да, но они не доступны. Также они не видимы и не доступны через интерфейс класса от которого они наследованы

    7. Назовите .Net класс от которого наследованы все остальные

    -System.Object

    8. Что означает термин immutable

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

    9. Как можно упорядочить элементы массива в обратном порядке

    — последовательно вызвать Sort() затем Reverse()

    10. Будет ли выполнен блок finally, если исключение не произойдет

    — Да

    11. Что такое делегат?
    — Делегат – это объект, инкапсулирующий ссылку на метод.
    Иными словами это тип, который ссылается на метод. Как только делегату назначен метод, он начинает работать точно также как и этот метод. Метод Делегат, может быть использован точно также как и любой другой метод с параметрами и возвращать значение.

    12. Какая разница между делегатом и событием?

    Delegate– это объект, инкапсулирующий ссылку на метод (указатель на функцию)
    Событие: Класс публикует событие, которое он может инициировать и любые классы могут подписаться на это событие.

    13.Что такое частные и общие сборки?
    Частные находятся в каталоге программы, а общие в GAC

    14.Что такое .Net Framework?
    Общеязыковая исполняющая среда common language runtime (CLR) + и библиотека классов framework class library (FCL).

    15.Какая разница между классами System.String и System.Text.StringBuilder
    Данные, хранящиеся в классе System.String и есть неизменяемые (immutable). Класс System.StringBuilder разрабатывался так, чтобы над изменяемой строкой можно было проделать множество операций. То есть при каждой операции над объектом класса System.String происходит перенос данных в новую область памяти, что влияет на производительность программы.

    16. Что такое строгая типизация (strong-typing) в сравнении со слабой типизацией (weak-typing)

    Weak type: Проверка типов переменных во время run time.

    17.Использование Assembly.Load — это статическая или динамическая ссылка?
    Динамическая загрузка assembly во время runtime. Метод из System.Reflection.

    18. Может ли DateTime равняться null? Почему?
    Нет, т.к. DateTime это тип значение, наследован из System.ValueType

    19. Как поменить метод в качестве устаревшего (obsolete)?
    [Obsolete(\»This is a message describing why this method is obsolete\»)] public int Foo()

    20. Сравните использование абстрактного базового класса и использование интерфейса?
    — Абстракный класс помимо абстрактных методов и свойств может содержать обычные методы, свойства, индексаторы и т.п. как и любой другой клас. Интерфейс же содержит только публичные абстрактные методы и свойства, без указания public и abstract

    21.Коллекции в .Net
    ArrayList(any type of object), Queue(FIFO: first-in, first-out), Stack (LIFO: last-in, first-out), StringCollection (like ArrayList, but elements are strings), BitArray(collection of boolean values),
    Hashtable

    22. Generic Collections
    List , Dictionary , Queue , Stack , SortedList , Collection , ReadOnlyCollection

    23. Методы сортировки элементов
    Пузырек(BubbleSort), Вставки(InsertSort), Шэлла(ShellSort), Пирамидальная(HeapSort), QuickSort (деление по полам, сортировка частей с рекурсией), Поразрядная (RadixSort)

    24.Методы расширения C# (IEnumerable)

    All, Any, Average, Cast, Concat, Contains, Cout, DefaultIfEmpty, Distinct, First, Last, Max, Min, Skip, Where, Union

    25. Как сделать перебор коллекции без foreach LINQ.
    var myColl = new Hashtable();
    > while (myEnum.Current != null)

    26. Что такое «Implicitly Typed Local Variables»
    Переменные объявленные с помощью var:

    27. Чем ссылочный тип отличается от размерного
    Размерный тип: Если некоторая переменная имеет размерный тип, она содержит реальные данные. Так что первое правило для размерных типов таково: они не могут быть null. При объявлении переменной размерного типа происходит выделение в стеке области. При присвоении значения переменной размерного типа в выделенное пространство в стеке помещается это значение.
    В С# определено несколько размерных типов, включая перечислители (enumerators), структуры (structures) и примитивы (primitives). Объявляя переменную одного из этих типов, вы каждый раз выделяете в стеке некоторое число байтов, ассоциированных с этим типом, и работаете напрямую с выделенным массивом битов. Кроме того, когда вы передаете переменную размерного типа, передается значение переменной, а не ссылка на лежащий в ее основе объект.

    Ссылочные типы: похожи на ссылки в C++, где они являются указателями, привязанными к типам (type-safe pointers). Это значит, что ссылка (если она не равна null) — это не просто адрес, который, как вы полагаете, может указывать (а может и не указывать) на определенный объект. Ссылка всегда гарантированно указывает объект заданного типа, уже выделенный в куче. Кроме того, ссылка может быть равна null. Как и в случае размерных типов, в С# несколько типов определены как ссылочные: классы, массивы, делегаты (delegates) и интерфейсы. Объявляя переменную одного из этих типов, вы каждый раз выделяете в куче некоторое ассоциированное с этим типом число байт. Но вместо того, чтобы работать с ними напрямую (как в случае размерных типов), вы работаете со ссылкой на выделенный объект.

    28.Что такое Упаковка и распаковка?

    В простейшем случае при упаковке размерный тип преобразуется в ссылочный. В обратном случае ссылочный тип распаковывается (unbox) в размерный. Замечательно в данной методике то, что объект лишь тогда является объектом, когда это необходимо. Допустим, вы объявляете переменную типа System.Int32. Для нее выделяется память в стеке. Вы можете передавать эту переменную любому методу, определенному в качестве принимающего аргументы типа System.Object, а также обращаться к любому из ее членов, к которому у вас есть доступ. Поэтому вы воспринимаете и ощущаете ее как объект. Но в реальности это всего 4 байта в стеке.
    Только когда вы пытаетесь использовать эту переменную согласно правилам, определенным интерфейсом базового класса System.Object, система автоматически упаковывает переменную, в результате чего она становится ссылочным типом и может быть использована так же, как любой объект. Упаковка — это механизм, посредством которого в С# любая сущность может быть представлена в виде объекта. Это позволяет избежать издержек, неизбежных в том случае, если б всякая сущность на самом деле была объектом.
    При упаковке (т. е. преобразовании из размерного типа в ссылочный) явного приведения типов не требуется. Однако при распаковке — преобразовании из ссылочного типа в размерный — приведение типов необходимо. Это так, потому что в случае распаковки объект может быть приведен к любому типу. Преобразование позволяет компилятору проверить, возможно ли приведение для заданного типа переменной. Поскольку приведение типов подчинено строгим правилам, определяемым CTS.
    int foo = 42; // Размерный тип.
    object bar = foo; // Переменная foo упакована в bar.
    int foo2 = (int)bar; // Распаковка и приведение к типу int.

    29. Что такое примитивы в C#
    C# примитивы это предопределенные внутренние типы. Несмотря на то, что примитивы определены в качестве зарезервированных слов C#, в конечном счете они являются псевдонимами для типов в определенных в классе .Net Framework. Примитива, это размерные типы которые хранят свои значения в стеке, за исключением типа string который является классом, а значит ссылочным типом.
    Несмотря на то, что примитивы это размерные типы, они являются объектами с интерфейсами и публичными методами: bool – System.Boolean, byte – System.Byte (8bit), char – System.Char (16 bit unicode), decimal – System.Decimal(128bit), double – System.Double(64 bit flt), float – System.single(32bit flt), int – System.Int32(32bit), long – System.Int64, sbyte – System.SByte(8bit), short – System.Int16, string – System.String, uint – System.UInt32, ulong – System.UInt64, ushort – System.UInt16.

    30. Какой тип является базовым для всех остальных типов.

    Корень всех типов «System.Object» В конечном счете все типы происходят от типа System.Object, что позволяет гарантировать наличие у каждого типа минимального набора функциональных возможностей. Все типы получают «бесплатно» четыре открытых метода: bool Equals(), int GetHashCode(), Type GetType(), string ToString.

    31. Что такое Атрибуты

    Атрибут – средство добавления ДЕКЛАРАТИВНОЙ информации к элементам программного кода. Назначение атрибутов – внесение всевозможных не предусмотренных обычным ходом выполнения приложения изменений:

    • описание взаимодействия между модулями;
    • дополнительная информация, используемая при работе с данными (управление сериализацией);
    • отладка;
    • и многое другое.

    Эта декларативная информация составляет часть метаданных кода. Она может быть использована при помощи механизмов отражения.
    Структура атрибута регламентирована. Атрибут – это класс. Общий предок всех атрибутов – класс System.Attribute.
    Предопределенные: DllImport, Serializable, NonSerialized, Obsolette, Coditional
    Пользовательские:
    using System; using Sc=System.Console;
    public class URLAttribute:Attribute <
    public URLAttribute(string url) < this.URL=url; >
    protected string uRL;
    public string URL < getset >
    >

    [URLAttribute(«www.xakep.ru»)]
    class HackerLink <>

    class Application <
    public static void Main() <
    Type type=typeof(HackerLink);
    foreach(Attribute attr in type.GetCustomAttributes(false)) <
    URLAttribute urlAttr=attr as URLAttribute;
    if(null!=urlAttr) Sc.WriteLine(«Target Link Is «+urlAttr.URL);
    >
    Sc.ReadLine();
    >
    >

    32. отражение — Reflection
    Отражение позволяет динамически определять сведения и данные о типах в программе.
    см. Пример из п.п. 31
    Type type=typeof(HackerLink);
    foreach(Attribute attr in type.GetCustomAttributes(false))
    < URLAttribute urlAttr=attr as URLAttribute; >


    33. Что не так со следующей строкой DateTime.Parse(myString);

    Не указывает локаль или формат
    34. Что такое «.Net Type System Unification»

    Основаная идея унификации типов, это обеспечить мост между ссылочными и размерными типами. В C# все типы, включая размерные наследованы от типа «System.Object». Таким образом становится возможным вызывать методы объекта для любого значения, даже для значений «примитивов» таких как int.

    35. Что такое MemberwiseClone() ?

    Метод MemberwiseClone производит поверхносное(shallow) копирование объекта, путем создания нового объекта и копирования не статических полей текущего объекта в новый объект. Если поле является размерным типом, производится побитовое копирование, если поле — ссылочного типа, ссылка копируется, но объект на который ссылаются — нет. Поэтому оргинальный объект и его клон ссылаются на один и тот же объект.
    36.

    37.

    38.

    39.

    40.

    Мастер Йода рекомендует:  Сервисы на NAROD.RU
    Добавить комментарий