Логические операции над булевскими операндами и целыми числами. Работа со шкалами
Рассмотрим логические операции, которые могут выполняться не только над булевскими значениями, но и над целыми числами. Высший приоритет среди этих операций имеет унарная операция отрицания (~x). Заметьте: есть две операции отрицания, одна из них (!x) определена только над операндами булевского типа, другая (~x) - только над целочисленными операндами.
Говоря о логических операциях над целыми числами, следует понимать, что целые числа можно рассматривать как последовательность битов (разрядов). Каждый бит, имеющий значение 0 или 1, можно интерпретировать как логическое значение обычным образом: 0 соответствует false, 1 - true. Логическая операция, применяемая к операндам одного и того же целочисленного типа, выполняется над соответствующими парами битов, создавая результат в виде последовательности битов и интерпретируемый как целое число. По этой причине такие логические операции называются побитовыми или поразрядными операциями.
Бинарных побитовых логических операций три - & , ^ , |. В порядке следования приоритетов это конъюнкция (операция "И"), исключающее ИЛИ, дизъюнкция (операция "ИЛИ"). Они определены как над целыми типами выше int, так и над булевыми типами. В первом случае они используются как побитовые операции, во втором - как обычные логические операции. Когда эти операции выполняются над булевскими операндами, то оба операнда вычисляются в любом случае, и если хотя бы один из операндов не определен, то и результат операции будет не определен. Когда необходима такая семантика логических операций, тогда без этих операций не обойтись.
Поразрядные логические операции определены не только над целыми числами, но и над перечислениями, которые проецируются на целочисленные типы. В реальных приложениях они чаще всего используются при работе со шкалами, часто представляемыми переменными перечислимого типа.
Побитовые логические операции широко применяются в реальном программировании при работе с так называемыми шкалами. Будем называть шкалой последовательность из n битов (n разрядов). Рассмотрим объект с n свойствами, каждым из которых объект может обладать или не обладать. Шкала позволяет однозначно задать, какими свойствами объект обладает, а какими нет. Пронумеруем свойства и будем записывать единицу в разряд с номером i, если объект обладает i-м свойством, и нуль - в противном случае.
Шкала позволяет экономно задавать информацию об объекте, а побитовые операции дают возможность весьма эффективно эту информацию обрабатывать. Поскольку эти операции определены над типами int, uint, long, ulong, C# может работать со шкалами длины 32 и 64.
Описание свойств объекта можно задать, используя перечисление - специальный тип данных, определяемый программистом. Свойства конкретного объекта, его шкалу можно задать переменной типа перечисление. При работе с такими переменными существенно используются поразрядные операции.
Рассмотрим содержательный пример. Пусть некоторая программистская фирма объявила прием на работу в фирме, предъявляя к претендентам такие требования: знание технологий и языков программирования. Возможный набор профессиональных свойств, которыми могут обладать претенденты на должность, можно задать перечислением:
// <summary> /// Свойства претендентов на должность программиста, /// описывающие знание технологий и языков программирования /// </summary> public enum Prog_Properties { VB = 1, C_sharp = 2, C_plus_plus = 4, Web = 8, Prog_1C = 16 }
Заметьте, при определении перечисления можно указать, на какое значение целого типа проецируется значение из перечисления. Если проецировать i-е значение на i-й разряд целого числа , как это сделано в примере, то переменные перечисления будут задавать шкалу свойств.
Свойства каждого претендента на должность характеризуются своей шкалой, которую можно рассматривать как переменную типа Prog_Properties. Задать шкалу претендента можно целым числом в интервале от 0 до , приведя значение к нужному типу. Например, так:
Prog_Properties candidate1 = (Prog_Properties)18;
Согласно шкале, этот кандидат знает язык C# и умеет работать в среде 1С. Более естественно шкалу кандидатов задавать с использованием логических операций над данными перечисления. Например, так:
Логические операции над шкалами позволяют эффективно реализовывать различные запросы, отбирая из массива кандидатов тех, кто соответствует заданным требованиям. Пусть, например, cand[i] - шкала i-го кандидата, а pattern - шкала, которая задает набор требований, предъявляемых к кандидатам. Рассмотрим условие:
(cand[i] & pattern) == pattern
Это условие будет истинным тогда и только тогда, когда кандидат соответствует всем требованиям, заданным в образце. Заметьте, скобки здесь необходимы, поскольку по умолчанию вначале бы выполнялась операция проверки на эквивалентность.
Этот простой пример показывает мощь аппарата шкал. Одно выражение с двумя операциями задает фильтр, позволяющий отобрать кандидата с нужными свойствами в условиях, когда число возможных свойств может быть велико. Без использования шкал потребовалось бы гораздо больше памяти и времени на достижение аналогичного результата.
Я написал отдельный класс Scales для работы со шкалами и перечислением Prog_Properties. Приведу несколько методов этого класса, позволяющих выполнять различные запросы к кандидатам.
/// <summary> /// Список кандидатов, которые обладают /// свойствами, заданными образцом. /// </summary> public ArrayList CandsHavePat() { ArrayList temp = new ArrayList(); for (int i = 0; i < n; i++) if ((cand[i] & pattern) == pattern) temp.Add("cand[" + i + "]"); return temp; } /// <summary> /// Список кандидатов, которые не обладают /// всеми свойствами, заданными образцом. /// </summary> public ArrayList CandsHaveNotAllPat() { ArrayList temp = new ArrayList(); for (int i = 0; i < n; i++) if ((~cand[i] & pattern) == pattern) temp.Add("cand[" + i + "]"); return temp; } /// <summary> /// Список кандидатов, которые обладают /// некоторыми свойствами, заданными образцом. /// </summary> public ArrayList CandsHaveSomePat() { ArrayList temp = new ArrayList(); for (int i = 0; i < n; i++) { currentScale = cand[i] & pattern; if (currentScale > 0 && currentScale < pattern) temp.Add("cand[" + i + "]"); } return temp; } /// <summary> /// Список кандидатов, которые обладают /// только свойствами, заданными образцом. /// </summary> public ArrayList CandsHaveOnlyPat() { ArrayList temp = new ArrayList(); for (int i = 0; i < n; i++) if (((cand[i] & pattern) == pattern) && ((cand[i] & ~pattern) == 0)) temp.Add("cand[" + i + "]"); return temp; }
Все эти методы устроены одинаково. Они отличаются условием отбора в операторе if, которое включает побитовые логические операции, выполняемые над шкалами cand и pattern, объявленными как массив переменных, и простой переменной перечислимого типа Prog_Properties. В качестве результата выполнения запроса возвращается массив типа ArrayList, который содержит список кандидатов, удовлетворяющих условиям запроса. На рис. 3.4 показаны результаты работы консольного приложения, в котором используется созданный класс и вызываются приведенные выше методы этого класса.
Рис. 3.4. Результаты запросов над шкалами
В C#, как и в C++, разрешены условные выражения. Конечно, без них можно обойтись, заменив их условным оператором. Вот простой пример их использования, поясняющий синтаксис их записи:
//Условное выражениеint a = 7, b = 9, max;max = (a > b) ? a : b;
Условное выражение начинается с условия, заключенного в круглые скобки, после которого следует знак вопроса и пара выражений, разделенных двоеточием " : ". Условие задается выражением типа bool. Если оно истинно, то из пары выражений выбирается первое, в противном случае результатом является значение второго выражения. В данном примере переменная max получит значение 9.
Заметьте, условное выражение является примером тернарного выражения - выражения с тремя операндами. И здесь, как и в случае условных логических операций, не требуется, чтобы все операнды были определены. Если булевский операнд определен и имеет значение true, то вычисляется второй операнд, а третий операнд не вычисляется и может быть в этот момент не определен.