От: | pilot.net | ||
Дата: | 24.06.15 10:00 | ||
Оценка: |
class data
{
...
typeData TypeData;
vector<double> getDoubleValues();
void setDoubleValues(vector<double>&)
vector<Float> getFloatValues();
void setFloatValues(vector<float>&)
...
}
if ( data.TypeData == ValueDouble)
{
return func<double>(data.getDoubleValues())
}else
if ( data.TypeData == ValueFloat)
{
return func<float>(data.getFloatValues())
}
От: | Mr.Delphist | ||
Дата: | 24.06.15 10:03 | ||
Оценка: |
PN>class data
PN>{
PN> ...
PN> typeData TypeData;
PN> vector<double> getDoubleValues();
PN> void setDoubleValues(vector<double>&)
PN> vector<Float> getFloatValues();
PN> void setFloatValues(vector<float>&)
PN> ...
PN>}
PN>
PN>if ( data.TypeData == ValueDouble)
PN>{
PN> return func<double>(data.getDoubleValues())
PN>}else
PN>if ( data.TypeData == ValueFloat)
PN>{
PN> return func<float>(data.getFloatValues())
PN>}
PN>
От: | velkin | https://kisa.biz | |
Дата: | 24.06.15 10:19 | ||
Оценка: |
От: | Kernan | https://rsdn.ru/forum/flame.politics/ | |
Дата: | 24.06.15 10:27 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 24.06.15 10:29 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 24.06.15 10:33 | ||
Оценка: |
От: | velkin | https://kisa.biz | |
Дата: | 24.06.15 10:33 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 24.06.15 10:34 | ||
Оценка: |
От: | uzhas | ||
Дата: | 24.06.15 10:38 | ||
Оценка: |
От: | velkin | https://kisa.biz | |
Дата: | 24.06.15 11:07 | ||
Оценка: |
typedef QVariantList
Synonym for QList<QVariant>.
QVariant | |
QVariant::Invalid 0 no type QVariant::BitArray 13 a QBitArray QVariant::Bitmap 73 a QBitmap QVariant::Bool 1 a bool QVariant::Brush 66 a QBrush QVariant::ByteArray 12 a QByteArray QVariant::Char 7 a QChar QVariant::Color 67 a QColor QVariant::Cursor 74 a QCursor QVariant::Date 14 a QDate QVariant::DateTime 16 a QDateTime QVariant::Double 6 a double QVariant::EasingCurve 29 a QEasingCurve QVariant::Font 64 a QFont QVariant::Hash 28 a QVariantHash QVariant::Icon 69 a QIcon QVariant::Image 70 a QImage QVariant::Int 2 an int QVariant::KeySequence 76 a QKeySequence QVariant::Line 23 a QLine QVariant::LineF 24 a QLineF QVariant::List 9 a QVariantList QVariant::Locale 18 a QLocale QVariant::LongLong 4 a qlonglong QVariant::Map 8 a QVariantMap QVariant::Matrix 80 a QMatrix QVariant::Transform 81 a QTransform QVariant::Matrix4x4 82 a QMatrix4x4 QVariant::Palette 68 a QPalette QVariant::Pen 77 a QPen QVariant::Pixmap 65 a QPixmap QVariant::Point 25 a QPoint QVariant::PointArray Polygon a QPointArray QVariant::PointF 26 a QPointF QVariant::Polygon 71 a QPolygon QVariant::Quaternion 86 a QQuaternion QVariant::Rect 19 a QRect QVariant::RectF 20 a QRectF QVariant::RegExp 27 a QRegExp QVariant::Region 72 a QRegion QVariant::Size 21 a QSize QVariant::SizeF 22 a QSizeF QVariant::SizePolicy 75 a QSizePolicy QVariant::String 10 a QString QVariant::StringList 11 a QStringList QVariant::TextFormat 79 a QTextFormat QVariant::TextLength 78 a QTextLength QVariant::Time 15 a QTime QVariant::UInt 3 a uint QVariant::ULongLong 5 a qulonglong QVariant::Url 17 a QUrl QVariant::Vector2D 83 a QVector2D QVariant::Vector3D 84 a QVector3D QVariant::Vector4D 85 a QVector4D QVariant::UserType 127 Base value for user-defined types. | |
QMetaType | |
QMetaType::Void 0 void QMetaType::Bool 1 bool QMetaType::Int 2 int QMetaType::UInt 3 unsigned int QMetaType::Double 6 double QMetaType::QChar 7 QChar QMetaType::QString 10 QString QMetaType::QByteArray 12 QByteArray QMetaType::VoidStar 128 void * QMetaType::Long 129 long QMetaType::LongLong 4 LongLong QMetaType::Short 130 short QMetaType::Char 131 char QMetaType::ULong 132 unsigned long QMetaType::ULongLong 5 ULongLong QMetaType::UShort 133 unsigned short QMetaType::UChar 134 unsigned char QMetaType::Float 135 float QMetaType::QObjectStar 136 QObject * QMetaType::QWidgetStar 137 QWidget * QMetaType::QVariant 138 QVariant QMetaType::QColorGroup 63 QColorGroup QMetaType::QCursor 74 QCursor QMetaType::QDate 14 QDate QMetaType::QSize 21 QSize QMetaType::QTime 15 QTime QMetaType::QVariantList 9 QVariantList QMetaType::QPolygon 71 QPolygon QMetaType::QColor 67 QColor QMetaType::QSizeF 22 QSizeF QMetaType::QRectF 20 QRectF QMetaType::QLine 23 QLine QMetaType::QTextLength 78 QTextLength QMetaType::QStringList 11 QStringList QMetaType::QVariantMap 8 QVariantMap QMetaType::QVariantHash 28 QVariantHash QMetaType::QIcon 69 QIcon QMetaType::QPen 77 QPen QMetaType::QLineF 24 QLineF QMetaType::QTextFormat 79 QTextFormat QMetaType::QRect 19 QRect QMetaType::QPoint 25 QPoint QMetaType::QUrl 17 QUrl QMetaType::QRegExp 27 QRegExp QMetaType::QDateTime 16 QDateTime QMetaType::QPointF 26 QPointF QMetaType::QPalette 68 QPalette QMetaType::QFont 64 QFont QMetaType::QBrush 66 QBrush QMetaType::QRegion 72 QRegion QMetaType::QBitArray 13 QBitArray QMetaType::QImage 70 QImage QMetaType::QKeySequence 76 QKeySequence QMetaType::QSizePolicy 75 QSizePolicy QMetaType::QPixmap 65 QPixmap QMetaType::QLocale 18 QLocale QMetaType::QBitmap 73 QBitmap QMetaType::QMatrix 80 QMatrix QMetaType::QTransform 81 QTransform QMetaType::QMatrix4x4 82 QMatrix4x4 QMetaType::QVector2D 83 QVector2D QMetaType::QVector3D 84 QVector3D QMetaType::QVector4D 85 QVector4D QMetaType::QQuaternion 86 QQuaternion QMetaType::QEasingCurve 29 QEasingCurve QMetaType::User 256 Base value for user types | |
QMetaType::Double 6 double
QMetaType::Float 135 float
От: | pilot.net | ||
Дата: | 24.06.15 12:13 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 24.06.15 12:16 | ||
Оценка: |
От: | uzhas | ||
Дата: | 24.06.15 14:06 | ||
Оценка: |
От: | LaptevVV | ||
Дата: | 24.06.15 14:34 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 25.06.15 04:30 | ||
Оценка: |
От: | pilot.net | ||
Дата: | 25.06.15 04:40 | ||
Оценка: |
void Core::checkCorePointBelonging(const Data & desc, const Data& core)
{
if (desc.isNull()) { return; }
auto descRef = desc.dataset().findReference();
if (descRef.isNull()) { return; }
if (core.isNull()) { return; }
auto coreRef = core.dataset().findReference();
if (coreRef.isNull()) { return; }
if (descRef.format() == FormatDouble)
{
auto d = descRef.getDoubleValues();
if (coreRef.format() == FormatDouble)
{
auto c = coreRef.getDoubleValues();
checkCorePointBelonging<double, double>(d, c, core, desc);
}
else
if (coreRef.format() == FormatFloat)
{
auto c = coreRef.getFloatValues();
checkCorePointBelonging<double, float>(d, c, core, desc);
}
}
else
if (descRef.format() == FormatFloat)
{
auto d = descRef.getFloatValues();
if (coreRef.format() == FormatDouble)
{
auto c = coreRef.getDoubleValues();
checkCorePointBelonging<float, double>(d, c, core, desc);
}
else
if (coreRef.format() == FormatFloat)
{
auto c = coreRef.getFloatValues();
checkCorePointBelonging<float, float>(d, c, core, desc);
}
}
}
От: | LaptevVV | ||
Дата: | 25.06.15 05:18 | ||
Оценка: |
Листинг 10.9 — явная проверка типа аргумента. Как у вас. Хотя у вас и поле-вектор внутри класса.Мультиметод — это виртуальная функция, выбираемая при выполнении на основании типа нескольких аргументов [26]. Как сказано в главе 9, в С++ есть два вида полиморфизма: статический и динамический. Статический полиморфизм реализуется перегрузкой функций и шаблонами функций. Виртуальные функции являются воплощением механизма динамического полиморфизма.
К сожалению, два этих вида полиморфизма реализованы в С++ совсем не одинаково. Например, при перегрузке нужный вариант функции выбирается на основании всего списка параметров — они совершенно равноправны. Выбор же виртуальной функции производится по единственному — левому — аргументу. Даже синтаксис вызова виртуальной функции отдает предпочтение объекту obj перед другими аргументами:
obj.function(аргументы)
Говорят, что виртуальная функция способна выполнить одиночную диспетчеризацию. Мультиметоды — это инструмент множественной диспетчеризации. Наиболее распространенный вариант — двойная диспетчеризация, которая означает, что нужная функция выбирается на основании типов двух аргументов. В книге А. Александреску [20] двойная диспетчеризация называется двойным переключением по типу.
Мультиметоды могут понадобиться в ситуациях, когда требуется выполнять некоторую операцию с различными сочетаниями типов параметров. Например, Элджер в [25] приводит пример с операцией сложения различных чисел: целых, действительных, комплексный. Б. Страуструп в [43] описывает ситуацию, когда нужно определять пересечение различных геометрических фигур, Брюс Эккель в [11] рассматривает операцию умножения между матрицами, векторами и скалярами, а Скотт Мейерс в [24] — сталкивающиеся объекты космической игры.
Использование RTTI
Покажем несколько схем реализации мультиметодов. Обычно классы, которые участвуют в работе мультиметодов, являются «родственниками», однако в некоторых случаях возможна реализация и между не родственными классами. Пусть у нас есть базовый абстрактный класс Base и три наследника Derived_a, Derived_b и Derived_c. Нам требуется выполнять некую операцию со всеми возможными сочетаниями параметров. В базовом классе определяется чистый абстрактный метод, который реализуется в производных классах. В общем случае для трех типов A, B и С число сочетаний любых двух типов равно 9:
1. Derived_a — Derived_a.
В конкретных ситуациях сочетаний бывает меньше, так как операция может оказаться коммутативной для некоторых сочетаний. Например, операция умножения скаляра и вектора — коммутативна, так же как и операция умножения скаляра на матрицу. Метод, выполняющий двойное переключение по типу, реализуется с параметрами базового класса — используется принцип подстановки. Двойное переключение эмулируются при помощи цепочек if...else с использованием преобразования dymamic_cast<>. Покажем сначала реализацию для двух классов (листинг 10.8), а потом добавим третий.
2. Derived_a — Derived_b.
3. Derived_a — Derived_c.
4. Derived_b — Derived_a.
5. Derived_b — Derived_b.
6. Derived_b — Derived_c.
7. Derived_c — Derived_a.
8. Derived_c — Derived_b.
9. Derived_c — Derived_c
Листинг 10.8. Двойной диспетчер для двух типов // определения классов class Base { public: virtual ~Base()=0; virtual bool Operator(const Base &R) = 0; // требуемая операция }; Base::~Base(){} // определение деструктора class Derived_a: public Base { public: virtual bool Operator(const Base &R); }; class Derived_b: public Base { public: virtual bool Operator(const Base &R); }; // реализация методов двойной диспетчеризации bool Derived_a::Operator(const Base &R) { if (const Derived_a *pra = dynamic_cast<const Derived_a*>(&R)) { // правый аргумент типа А; обработка варианта "A-A" cout << "A-A" << endl; return true; } else if (const Derived_b *prb = dynamic_cast<const Derived_b*>(&R)) { // правый аргумент типа В; обработка варианта "A-В" cout << "A-B" << endl; return true; } else throw exception ("Error! Incorrect type argument!"); } bool Derived_b::Operator(const Base &R) { if (const Derived_a *pra = dynamic_cast<const Derived_a*>(&R)) { // правый аргумент типа А; обработка варианта "B-A" cout << "B-A" << endl; return true; } else if (const Derived_b *prb = dynamic_cast<const Derived_b*>(&R)) { // правый аргумент типа В; обработка варианта "В-В" cout << "B-B" << endl; return true; } else throw exception ("Error! Incorrect type argument!"); }
Мультиметоды устроены достаточно просто: проверяется правый операнд и в зависимости от его типа выполняется обработка. Если же правый операнд неизвестного типа, то генерируется исключение. Добавим третий класс и покажем механизм использования оператора typeid() для распознавания типа (листинг 10.9).
Листинг 10.9. Мультиметод нового дополнительного класса class Derived_c: public Base { public: virtual bool Operator(const Base &R); }; bool Derived_c::Operator(const Base &R) { if (typeid(Derived_a) == typeid(R)) { // правый аргумент типа А; обработка варианта "С-A" cout << "С-A" << endl; return true; } else if (typeid(Derived_b) == typeid(R)) { // правый аргумент типа В; обработка варианта "С-В" cout << "С-В" << endl; return true; } else if (typeid(Derived_c) == typeid(R)) { // правый аргумент типа С; обработка варианта "С-С" cout << "С-С" << endl; return true; } else throw exception ("Error! Incorrect type argument!"); }
Если требуется, чтобы выполнялась обработка объектов типа C методами классов A и B, нужно модифицировать их, добавив проверку правого аргумента на совпадение с типом С. Но делать это в обязательном порядке нет необходимости — все определяется конкретной задачей. Таким образом, у нас реализовано 7 сочетаний из 9, в чем можно убедиться, выполнив такой фрагмент программы:
Derived_a A; Derived_b B; Derived_c C; try { cout << A.Operator(A) << endl; cout << A.Operator(B) << endl; cout << B.Operator(A) << endl; cout << B.Operator(B) << endl; cout << C.Operator(A) << endl; cout << C.Operator(B) << endl; cout << C.Operator(C) << endl; cout << A.Operator(C) << endl; // вызывает исключение } catch(exception &e) { cout << e.what()<< endl; }
Последний оператор вызывает исключение, так как сочетание Derived_a — Derived_c не реализовано.
Задачу мы решили — реализовали двойное переключение по типу. Однако даже начинающий программист понимает, что у этого решения есть недостатки. Например, задействован механизм RTTI, который не является самым эффективным в С++. Но это не самое важное. Гораздо хуже, что нам при добавлении нового класса приходится модифицировать тексты методов. Представьте, что классов у нас хотя бы 7–8, и методов, соответственно, тоже. То есть в каждом методе требуется выполнять до 7–8 однотипных проверок. Добавление нового класса может превратиться в кошмар, поэтому поищем другие пути реализации двойной диспетчеризации.
От: | LaptevVV | ||
Дата: | 25.06.15 05:22 | ||
Оценка: |
Использование только виртуальных функций
Есть очень нетривиальный способ реализовать двойное переключение по типу с использованием только виртуальных функций. Как обычно, сначала реализуем мультиметоды для двух классов (листинг 10.10), а затем попытаемся добавить третий. Основная идея состоит в том, чтобы реализовать двойную диспетчеризацию как две одиночных, выполняемых последовательно; первый виртуальный вызов определяет динамический тип первого объекта, а второй — второго объекта.
Листинг 10.10. Мультиметоды — только виртуальные функции // предварительное объявление наследников class Derived_a; class Derived_b; class Derived_c; // абстрактный базовый класс class Base { public: virtual ~Base()=0; virtual bool Operator(const Base &R)const = 0; virtual bool Operator(const Derived_a &R)const = 0; virtual bool Operator(const Derived_b &R)const = 0; }; Base::~Base(){} // определение классов-наследников class Derived_a: public Base { public: virtual bool Operator(const Base &R) const { return R.Operator(*this); }; // перенаправление виртуальности virtual bool Operator(const Derived_a &R)const { cout << "A-A" << endl; return true; } virtual bool Operator(const Derived_b &R)const { cout << "A-B" << endl; return true; } }; class Derived_b: public Base { public: virtual bool Operator(const Base &R)const { return R.Operator(*this); }; // перенаправление виртуальности virtual bool Operator(const Derived_a &R)const { cout << "B-A" << endl; return true; } virtual bool Operator(const Derived_b &R)const { cout << "B-B" << endl; return true; } };
Обратите внимание на реализацию первой функции, которая абсолютно одинакова в обоих классах — именно она реализует двойное переключение по типу:
virtual bool Operator(const Base &R) const { return R.Operator(*this); };
Чтобы было понятно, где и как она работает, рассмотрим фрагмент программы:
Derived_a A; Derived_b B; // нет необходимости в виртуальности cout << A.Operator(A) << endl; cout << A.Operator(B) << endl; cout << B.Operator(A) << endl; cout << B.Operator(B) << endl; Base &D = A; cout << A.Operator(D) << endl; // работают 2 виртуальных вызова cout << B.Operator(D) << endl; // работают 2 виртуальных вызова cout << D.Operator(D) << endl; // работают 2 виртуальных вызова cout << D.Operator(A) << endl; // работает 1 виртуальный вызов cout << D.Operator(B) << endl; // работает 1 виртуальный вызов
В первых четырех вызовах нет необходимости в двойной диспетчеризации, так как нет подстановки ссылки на наследника вместо ссылки на базовый класс — вызываются сразу нужные функции. Эти функции и выводят на экран строки, соответствующие типам операндов:
А-А
А-В
В-А
В-В
Следующие три вызова как раз и выполняют двойную диспетчеризацию. А первом варианте мы сначала попадаем в первый метод класса Derived_a, так как тип левого аргумента именно этот. При этом на место параметра-ссылки R подставляется фактический аргумент-ссылка D, инициализированный ссылкой A типа Derived_a, а *this внутри метода статически имеет тип Derived_a. Таким образом, вызов превращается в A.Operator(A), и на экран выводится строка «А-А». Второй вариант вызова сначала приводит нас к первой функции класса B, так как левый аргумент вызова имеет тип Derived_b. Снова на место ссылки R подставляется ссылка D, инициализированная ссылкой типа Derived_a. Однако мы находимся внутри класса Derived_b, поэтому *this имеет тип Derived_b и вызов превращается в A.Operator(B), что мы и наблюдаем при выводе на экран строки «А-В». В третьем случае сначала виртуализируется левый аргумент, и вызов направляется в класс Derived_a, а дальше процесс продолжается по первому варианту и на экран выводится строка «А-А».
Четвертый виртуальный вызов превращается в A.Operator(A), а пятый — в A.Operator(B). Виртуальность работает только один раз — для левого аргумента.
Теперь проделаем то же самое с указателями:
Base *pA = new Derived_a(); Base *pB = new Derived_b(); cout << pA->Operator(*pA) << endl; cout << pA->Operator(*pB) << endl; cout << pB->Operator(*pA) << endl; cout << pB->Operator(*pB) << endl;
Первый вариант, естественно, сначала приводит нас в первый метод класса Derived_a, который вызывает второй метод того же класса с аргументом того же типа. Второй вызов тоже сначала сопоставляется с первым методом класса Derived_a, но поскольку в этом случае на место R поставляется ссылка на объект типа Derived_b, а *this имеет тип Derived_a, то вызывается метод с прототипом
virtual bool Operator(const Derived_a &R) const;
На экране, естественно, получаем строку «В-А». Третий и четвертые варианты, естественно, первый вызов осуществляют из класса Derived_b, а второй — в соответствии с аргументом *this.
Таким образом, методы с параметром типа Base осуществляют то самое двойное переключение по типу.
Этот способ реализации мультиметодов имеет довольно значительные преимущества по сравнению с предыдущим. Во-первых, нет необходимости генерировать исключение при неправильных типах аргументов, поскольку все проверки осуществляет компилятор. Во-вторых, обратите внимание на значительное упрощение текста методов — реализуется только фактически необходимая работа без всяких лишних проверок.
Теперь добавим третий класс и посмотрим, какие изменения придется внести в исходные классы:
class Derived_c: public Base { public: virtual bool Operator(const Base &R)const { return R.Operator(*this); } virtual bool Operator(const Derived_a &R)const { cout << "C-A" << endl; return true; } virtual bool Operator(const Derived_b &R)const { cout << "C-B" << endl; return true; } virtual bool Operator(const Derived_c &R)const { cout << "C-C" << endl; return true; } };
Этот класс реализует нам операцию с левым операндом типа Derived_c. Отметим, что ни класс Derived_a, ни класс Derived_b не работают с аргументом типа Derived_c. Не будем вносить никаких изменений в эти классы, и выполним небольшой фрагмент программы:
Derived_a A; Derived_b B; Derived_c C; Base &rA = A; Base &rB = B; Base &rC = C; cout << A.Operator(C) << endl; cout << B.Operator(C) << endl; cout << rA.Operator(rC) << endl; cout << rB.Operator(rC) << endl;
Никаких ошибок компиляции нет! Несмотря на то, что мы не определяли в классах Derived_a и Derived_b операции с аргументом типа Derived_c, следующие вызовы не вызывают ошибок компиляции:
cout << A.Operator(C) << endl; cout << B.Operator(C) << endl;
Причина в том, что в данном случае работает принцип подстановки. Ввиду отсутствия метода с аргументом типа Derived_c выбирается метод класса с параметром-ссылкой базового типа и на его место подставляется ссылка на наследника. Далее выполняется механизм двойной диспетчеризации. Точно так же работают вызовы:
cout << rA.Operator(rC) << endl; cout << rB.Operator(rC) << endl;
Если нас это устраивает, никаких изменений в классы Derived_a и Derived_b вносить нет необходимости. Если же требуется специальный вид операции A.Operator(C), то нужно добавить соответствующий виртуальный метод в класс Derived_a.
Как уже было сказано, подобная методика работает и для классов, не являющихся родственниками. Построим следующую иерархию классов (листинг 10.11).
Листинг 10.11. Мультиметоды для классов, не являющихся родственниками class B; // объявление корня второй иерархии class A // корень первой иерархии { public: virtual ~A(){} virtual void Operator(B*)=0; }; class D1A; // объявления классов первой иерархии class D2A; class B // корень второй иерархии { public: virtual void Operator(D1A& rA) = 0; virtual void Operator(D2A& rA) = 0; }; // классы иерархии А class D1A: public A { public: virtual void Operator(B* R) { R->Operator(*this); } }; class D2A: public A { public: virtual void Operator(B* R) { R->Operator(*this); } }; // классы иерархии В class D1B: public B { public: virtual void Operator(D1A& rA) { cout << "D1A!" << endl; }; virtual void Operator(D2A& rA) { cout << "D2A!" << endl; }; }; class D2B: public B { public: virtual void Operator(D1A& rA) { cout << "D1A!" << endl; }; virtual void Operator(D2A& rA) { cout << "D2A!" << endl; }; };
Две иерархии — А и В — не имеют общих родственников. Тем не менее, методика последовательных виртуальных вызовов, описанная ранее для одной иерархии, работает точно так же. В данном случае все требуемые действия реализованы во второй иерархии, а первая только осуществляет первый шаг двойного переключения по типу. Небольшой фрагмент программы иллюстрирует необходимые переключения:
D1A a1; D2A a2; B *pb = new D1B(); // указатель на базовый класс <- адрес наследника a1.Operator(pb); // работают 2 виртуальных вызова a2.Operator(pb); // работают 2 виртуальных вызова pb->Operator(a1); // работает 1 виртуальный вызов pb = new D2B(); a1.Operator(pb); // работают 2 виртуальных вызова a2.Operator(pb); // работают 2 виртуальных вызова pb->Operator(a1); // работает 1 виртуальный вызов
Двойное переключение по типу — это основа паттерна Visitor (посетитель). Это — один из наиболее трудных для понимания паттернов, недаром в [17] он описан последним.
От: | pilot.net | ||
Дата: | 25.06.15 05:30 | ||
Оценка: |
От: | LaptevVV | ||
Дата: | 25.06.15 05:38 | ||
Оценка: |