пятница, 28 ноября 2014 г.

Ссылка. DI vs. DIP vs. IoC

http://sergeyteplyakov.blogspot.ru/2014/11/di-vs-dip-vs-ioc.html

"Большинство разработчиков не различает DI и DIP, хотя за каждой из этих аббревиатур скрываются разные понятия."

Это про меня :-)

среда, 26 ноября 2014 г.

И в третий раз не взяли статью на Хабр.

И в третий раз  не взяли статью на Хабр.

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

Пидарасы..

Про NSString и std::string

По мотивам  - Скорее "филосовское". Почему синтаксический сахар в виде парадигмы property не востребован в классическом C++?

Пока приведу лишь код:

//#define UseMutable

template<typename DataType>
    class ObjCObjectHolder {
    protected:
        DataType rData = nil;
    protected:
        #ifdef UseMutable
        virtual
        #endif
        void setData (DataType aData)
        {
            ASSIGN(rData, aData);
        }
        
    public:
        ObjCObjectHolder () : rData(nil)
        {
            
        }
        
        ObjCObjectHolder (DataType aData) : rData(nil)
        {
            setData(aData);
        }
        
        ObjCObjectHolder (const ObjCObjectHolder & aCopy) : rData(nil)
        {
            setData(aCopy.rData);
        }
        
        const ObjCObjectHolder & operator = (const ObjCObjectHolder & aCopy)
        {
            setData(aCopy.rData);
            return *this;
        }
        
        const ObjCObjectHolder & operator = (DataType aCopy)
        {
            setData(aCopy);
            return *this;
        }
        
        virtual ~ObjCObjectHolder ()
        {
            DESTROY(rData);
        }
    }; // ObjCObjectHolder
; // template<typename DataType>

#ifdef UseMutable
typedef NSMutableString NSStringHolderString;
#else
typedef NSString NSStringHolderString;
#endif

typedef ObjCObjectHolder<NSStringHolderString *> NSStringHolderPrim;

class NSStringHolder : public NSStringHolderPrim
{
#ifdef UseMutable
protected:
    virtual void setData (NSMutableString * aData) override
    {
        if (aData)
            ASSIGN(rData, [NSMutableString stringWithString: aData]);
        else
            ASSIGN(rData, [NSMutableString stringWithCapacity: 1024]);
    }
    
    void setData (NSString * aData)
    {
        assert(aData);
        ASSIGN(rData, [NSMutableString stringWithString: aData]);
    }
#endif
public:
    NSStringHolder ()
    {
        
    }
    
    NSStringHolder (NSString * aString)
    {
        setData(aString);
    }
    
    NSStringHolder (const std::string & aValue)
    {
        NSStringHolderString *vStr = [[NSStringHolderString alloc] initWithBytes: aValue.c_str() length: aValue.length() encoding: NSWindowsCP1251StringEncoding];
        @try {
            setData(vStr);
        }
        @finally {
            DESTROY(vStr);
        }
    }
    
    NSStringHolder (const char * aValue)
    {
        NSStringHolderString *vStr = [[NSStringHolderString alloc] initWithBytes: aValue length: strlen(aValue) encoding: NSWindowsCP1251StringEncoding];
        @try {
            setData(vStr);
        }
        @finally {
            DESTROY(vStr);
        }
    }
    
    size_t length () const
    {
        return [rData length];
    }
    
    __strong const char * c_str() const
    {
        if (rData)
            return [rData cStringUsingEncoding: NSWindowsCP1251StringEncoding];
        else
            return "";
    }
    
    operator NSString * () const
    {
        return rData;
    }
    
    operator std::string () const
    {
        if (rData)
            return std::string([rData cStringUsingEncoding: NSWindowsCP1251StringEncoding]);
        else
            return std::string("");
    }
    
    const NSStringHolder & operator = (NSString * aValue)
    {
        setData(aValue);
        return *this;
    }
    
    const NSStringHolder & operator = (const char * aValue)
    {
        NSStringHolderString *vStr = [[NSStringHolderString alloc] initWithBytes: aValue length: strlen(aValue) encoding: NSWindowsCP1251StringEncoding];
        @try {
            setData(vStr);
        }
        @finally {
            DESTROY(vStr);
        }
        return *this;
    }
    
    const NSStringHolder & operator = (const std::string & aValue)
    {
        NSStringHolderString *vStr = [[NSStringHolderString alloc] initWithBytes: aValue.c_str() length: aValue.length() encoding: NSWindowsCP1251StringEncoding];
        @try {
            setData(vStr);
        }
        @finally {
            DESTROY(vStr);
        }
        return *this;
    }
    
    void append (NSString * aValue)
    {
        if (aValue) {
            if (rData) {
                #ifdef UseMutable
                    [rData appendString: aValue];
                #else
                    setData([rData stringByAppendingString: aValue]);
                #endif
            }
            else
                setData(aValue);
        }
    }
    
    void append (const NSStringHolder & aValue)
    {
        append(aValue.rData);
    }
    
    const NSStringHolder & operator += (NSString * aValue)
    {
        append(aValue);
        return *this;
    }
    
    const NSStringHolder & operator += (const NSStringHolder & aValue)
    {
        append(aValue);
        return *this;
    }
    
    void trim ()
    {
        assert(rData);
        setData([rData stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]);
    }
    
    NSStringHolder substringToIndex (size_t aPos) const
    {
        assert(rData);
        NSStringHolder vSubstr ([rData substringToIndex: aPos]);
        return vSubstr;
    }
    
    NSStringHolder substringFromIndex (size_t aPos) const
    {
        assert(rData);
        NSStringHolder vSubstr ([rData substringFromIndex: aPos]);
        return vSubstr;
    }
    
    void insert (size_t aPos, const NSStringHolder & aValue)
    {
        if (aValue.rData) {
            if (rData) {
                if (aPos == 0) {
                    setData([(NSString *)aValue stringByAppendingString: rData]);
                }
                else
                if (aPos == rData.length) {
                    setData([rData stringByAppendingString: aValue]);
                }
                else {
                    NSString *vStr = [rData substringToIndex: aPos];
                    vStr = [vStr stringByAppendingString: aValue];
                    vStr = [vStr stringByAppendingString: [rData substringFromIndex: aPos]];
                    setData(vStr);
                }
            }
            else
                setData(aValue.rData);
        }
    }
    
    const NSStringHolder & operator += (const char * aValue)
    {
        append(aValue);
        return *this;
    }
    
    const NSStringHolder & operator += (const std::string & aValue)
    {
        append(aValue);
        return *this;
    }
    
    void resize (size_t aNewLength)
    {
        if (aNewLength < [rData length])
            setData([rData substringToIndex: aNewLength/* + 1*/]);
        else
        if (aNewLength > [rData length])
            assert(false);
    }
    
    size_t find_last (NSString * aWhatFind, size_t aPos) const
    {
        NSRange vRange = [rData rangeOfString: aWhatFind options: NSBackwardsSearch range: NSMakeRange(0, aPos + 1)];
        if (vRange.location == NSNotFound)
            return std::string::npos;
        else
            return vRange.location;
    }
    
    size_t find_first (NSString * aWhatFind, size_t aPos) const
    {
        if (aPos == std::string::npos)
            return std::string::npos;
        NSRange vRange = [rData rangeOfString: aWhatFind options: 0 range: NSMakeRange(aPos, rData.length - aPos)];
        if (vRange.location == NSNotFound)
            return std::string::npos;
        else
            return vRange.location;
    }
    
    size_t find_first (const NSStringHolder & aWhatFind, size_t aPos) const
    {
        if (aPos == std::string::npos)
            return std::string::npos;
        NSRange vRange = [rData rangeOfString: aWhatFind options: 0 range: NSMakeRange(aPos, rData.length - aPos)];
        if (vRange.location == NSNotFound)
            return std::string::npos;
        else
            return vRange.location;
    }
    
    size_t find (const NSStringHolder & aWhatFind) const
    {
        NSRange vRange = [rData rangeOfString: aWhatFind];
        if (vRange.location == NSNotFound)
            return std::string::npos;
        else
            return vRange.location;
    }
    
    bool operator != (const NSStringHolder & aValue) const
    {
        return ![rData isEqualToString: aValue.rData];
    }
    
    bool operator != (NSString * aValue) const
    {
        return ![rData isEqualToString: aValue];
    }
    
    bool operator == (const NSStringHolder & aValue) const
    {
        return [rData isEqualToString: aValue.rData];
    }
    
    bool operator == (NSString * aValue) const
    {
        return [rData isEqualToString: aValue];
    }
    
    const unichar operator [] (size_t anIndex) const
    {
        return [rData characterAtIndex: anIndex];
    }
    
    const NSStringHolder operator + (const NSStringHolder & anOther) const
    {
        NSStringHolder vOther (*this);
        vOther += anOther;
        return vOther;
    }
    
    const NSStringHolder operator + (const char * anOther) const
    {
        NSStringHolder vOther (*this);
        vOther += anOther;
        return vOther;
    }
    
    void appendLong (long aValue)
    {
        append([NSString stringWithFormat: @"%ld", aValue]);
    }
    
    void appendLongLong (long long aValue)
    {
        append([NSString stringWithFormat: @"%lld", aValue]);
    }
    
    void appendUnsignedLong (unsigned long aValue)
    {
        append([NSString stringWithFormat: @"%ld", aValue]);
    }
    
    void appendUnsignedLongLong (unsigned long long aValue)
    {
        append([NSString stringWithFormat: @"%lld", aValue]);
    }
}; // NSStringHolder

- всё.


Что такое NSStringHolder? Очень просто - это замена std::string для Objective-C.

Скорее "филосовское". Почему синтаксический сахар в виде парадигмы property не востребован в классическом C++?

Ни на что не претендую. И ни к чему не призываю.

Просто размышляю.

Я не собираюсь делать из C++ Delphi или наоборот. Всё написанное ниже представляет лишь "академический интерес".

Почему синтаксический сахар в виде парадигмы property не востребован в классическом C++?

Я вот совсем не сторонник холиваров. Я стремлюсь мыслить трезво.

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

Например меня долгое время мучил вопрос - "почему в C++ нет конструкции try..finally".

Но поскольку C++ разрабатывали более чем умные люди, то я сразу подозревал, что это "не просто так".

Этот вопрос хорошо освещён тут - Ссылка. Why doesn't C++ provide a "finally" construct?

Также меня долгое время мучил вопрос - "почему в C++ нету парадигмы property".

Особенно учитывая тот факт, что в расширениях C++ Builder'а эта парадигма - есть.

Но опять же - поскольку C++ разрабатывали более чем умные люди, то я подозревал, что это "не просто так".

И вот к каким выводам я не так давно пришёл.

Потому, что в C++ есть собственный "синтаксический сахар" в виде ссылок на значения, шаблонов и переопределения операторов.

Переопределения операторов "бездумно" - я не люблю, но при вдумчивой разработке библиотек - они очень и очень востребованы.

Итак.

Пусть у нас есть класс SomeClass на C++:

class SomeClass
{
public:
 std::string a;
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.a;
X.a = "SomeString";

- ну тут всё понятно.

Есть - класс SomeClass.

У него есть член данных a - его можно писать и читать.

Теперь задумаемся о инкапсуляции данных.

Как "бедные" программисты на C++ (на взгляд Delphi программистов) обычно "вынуждены" писать?

Ну как-то так:

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string get_a () const
 {
  return m_a;
 }

 void set_a (const std::string & aValue)
 {
  m_a = aValue;
 }
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.get_a();
X.set_a("SomeString");

Сразу "бросается в глаза" копирование значения.

Это легко исправить, вернув ссылку (std::string &), а не значение (std::string):

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string & get_a () const
 {
  return m_a;
 }

 void set_a (const std::string & aValue)
 {
  m_a = aValue;
 }
}; // SomeClass

Тут на сцену выходят ссылки. Они - тут главные.

Но пока на время забудем о них.

Немного потеоретизируем на тему "как было бы хорошо иметь property в C++".

Ну скажем так:

class SomeClass
{
private:
 std::string m_a;
private:
 const std::string get_a () const
 {
  return m_a;
 }

 void set_a (const std::string & aValue)
 {
  m_a = aValue;
 }
public:
 property std::string a
  read get_a
  write set_a;
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.a;
X.a = "SomeString";

Красиво? Красиво.

Но зададимся вопросом - "зачем ?".

Вспомним про нашу триаду - ссылки, шаблоны, переопределения операторов.

А также вспомним о перегрузке функций.

Перепишем наш класс так:

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string a () const
 // - это getter
 {
  return m_a;
 }

 std::string & a ()
 // - это setter
 {
  return m_a;
 }
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.a();
X.a() = "SomeString";

Красиво? По-моему - тоже красиво.

Ну, а лишние скобочки - () это "не вопрос".

Всё?

Не совсем.

Встаёт вопрос.

А если мы хотим написать setter в виде:

 void set_a (const std::string & aValue)
 {
  if (this->someCondition(aValue))
   m_a = aValue;
 }

- что тогда делать?

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

Такая возможность у нас всегда есть:

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string a () const
 // - это getter
 {
  return m_a;
 }

 void set_a (const std::string & aValue)
 // - это setter
 {
  if (this->someCondition(aValue))
   m_a = aValue;
 }
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.a();
X.set_a("SomeString");

- но как сделать "красиво"?

Тут вспомним о шаблонах и перегрузке операторов,

Напишем вспомогательный шаблонный класс ValueSetter:

template<class DataOwner, typename DataType>
 class ValueSetter
 {
  private:
   DataType & m_data;
   DataOwner * m_owner;
  public
   ValueSetter (DataOwner * aOwner, DataType & aData) : m_data(aData), m_owner(aOwner)
   // - инициализирующий конструктор
   {
   }

   ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data), m_owner(anOther.m_owner)
   // - копирующий конструктор
   {
   }

   ValueSetter & operator= (const DataType & aValue)
   // - оператор присваивания
   {
    if (m_owner->someCondition(aValue))
     m_data = aValue;
    return *this;
   }
 } ; // ValueSetter
; //template

И тогда наш класс приобретает вид:

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string a () const
 // - это getter
 {
  return m_a;
 }

 typedef ValueSetter<SomeClass, std::string> Setter_for_a;

 Setter_for_a & a ()
 // - это setter
 {
  Setter_for_a vSetter(this, a);
  return vSetter;
 }
}; // SomeClass

И его использование:

SomeClass X;

std::string Y = X.a();
X.a() = "SomeString";

Теперь - как параметризовать someCondition?

Приведу ссылку - std::sort.

Давайте немного перепишем класс ValueSetter (тут я могу путаться в синтаксисе, но надеюсь, что смысл понятен):

template<class DataOwner, typename DataType, typename Condition>
 class ValueSetter
 {
  private:
   DataType & m_data;
   DataOwner * m_owner;
  public
   ValueSetter (DataOwner * aOwner,DataType & aData) : m_data(aData), m_owner(aOwner)
   // - инициализирующий конструктор
   {
   }

   ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data), m_owner(anOther.m_owner)
   // - копирующий конструктор
   {
   }

   ValueSetter & operator= (const DataType & aValue)
   // - оператор присваивания
   {
    if (Condition(m_owner, aValue))
     m_data = aValue;
    return *this;
   }
 } ; // ValueSetter
; //template

И наш класс приобретает вид:

class SomeClass
{
private:
 std::string m_a;
 static bool condition_for_a (const SomeClass & aThis, const std::string & aValue)
 {
  return aThis.someCondition(aValue);
 }
public:
 const std::string a () const
 // - это getter
 {
  return m_a;
 }

 typedef ValueSetter<SomeClass, std::string, condition_for_a> Setter_for_a;

 Setter_for_a & a ()
 // - это setter
 {
  Setter_for_a vSetter(this, a);
  return vSetter;
 }
}; // SomeClass

А ещё ведь можно использовать лямбды.

 C++11, передача Lambda-выражения как параметра.
Лямбда выражения теперь и в С++.
C++0x (С++11). Лямбда-выражения.

Могу что-то путать, но тогда наш ValueSetter приобретает вид:

template<typename DataType, typename Condition>
 class ValueSetter
 {
  private:
   DataType & m_data;
  public
   ValueSetter (DataType & aData) : m_data(aData)
   // - инициализирующий конструктор
   {
   }

   ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data)
   // - копирующий конструктор
   {
   }

   ValueSetter & operator= (const DataType & aValue)
   // - оператор присваивания
   {
    if (Condition(aValue))
     m_data = aValue;
    return *this;
   }
 } ; // ValueSetter
; //template

И класс SomeClass приобретает вид:

class SomeClass
{
private:
 std::string m_a;
public:
 const std::string a () const
 // - это getter
 {
  return m_a;
 }

 typedef ValueSetter<std::string, [&] (const std::string & aValue) ->bool { return this->someCondition(aValue); }> Setter_for_a;

 Setter_for_a & a ()
 // - это setter
 {
  Setter_for_a vSetter(a);
  return vSetter;
 }
}; // SomeClass

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

Чтобы было понятно "как оно работает".

Ведь далее связку m_a и const std::string a () с Setter_for_a & a () можно упаковать в ещё один шаблон.

И вынести все эти шаблоны в "стандартную библиотеку".

Было бы желание.

Ведь чем силён C++? Именно стандартными библиотеками и возможностью написания своих. С шаблонизацией. Те. мета-программированием. Т.е. с внесением "собственного синтаксического сахара".

Насчёт передачи функций и лямбд в шаблон - повторюсь, могу путать -посему критика на сей счёт - приветствуется. Пишу дома, где компилятора C++ - нет. Завтра на работе - проверю. Если будет время.

Ну и собственно есть практический вопрос к аудитории - как мне правильно написать шаблонный класс, который использует лямбду, которая использует один из параметров шаблона?

В этом вопросе - я пока "плаваю". Буду пытаться компилировать код.

Ну и коротко - если мы хотим "индексированные свойства", то что делать?

А всё просто:

class SomeClass1
{
private:
 std::vector<std::string> m_a;
public:
 const std::string & a () const
 {
  return m_a;
 }

 std::string & a ()
 {
  return m_a;
 }
}; // SomeClass1

И использование:

SomeClass1 X;

std::string Y = X.a()[0];
X.a()[1] = "Some Value";

Ну и если на SomeClass1 определить оператор [] вот так:

class SomeClass1
{
private:
 std::vector<std::string> m_a;
public:
 const std::string & a () const
 {
  return m_a;
 }

 std::string & a ()
 {
  return m_a;
 }
 
 const std::string & operator[] (size_t anIndex) const
 {
  return m_a[anIndex];
 }

 std::string & operator[] (size_t anIndex)
 {
  return m_a[anIndex];
 }
}; // SomeClass1

- то можно написать и так:

SomeClass1 X;

std::string Y = X[0];
X[1] = "Some Value";

Или даже так:

SomeClass1 X;

char Y = X[0][1];
X[1][0] = 'c';

Ну а "всё остальное" - тоже через ValueSetter.

Вот собственно и всё пока.

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

А теперь об интересе не академическом, а вполне себе практическом.

В следующей заметке я напишу, как путём примерно такой же техники wraper'ов и holder'ов заменить использование std::string на NSString *, без перелопачивания "тонн кода".

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

Спросите - зачем?

Отвечу ссылкой - Коротко. Про STL под iOS/MacOS.

Просто у нас было какое-то количество алгоритмов работающих с std::string и прочими объектами STL, которые "захотелось" "малой кровью" переделать на NSObject, NSArray, NSString, NSData и NSDistionary.

Ну не то, чтобы прям захотелось, просто "жизнь приказала".

Но на сегодня - пока всё.

P.S. Ну и в тему "синтаксического сахара" - Скорее "для себя". Классы в скриптовой машине.

P.P.S. http://programmingmindstream.blogspot.ru/2014/11/property-c.html?showComment=1417552354575#c8460879686358984674

"Я просто задался вопросом - "как я бы на месте Borland'а реализовывал бы __property не ломая компилятора и оставаясь в рамках ANSI стандарта". Только и всего :-)

В реальной жизни "именно это" я применять - никоим образом не собираюсь."

Offtopic. Продолжим вечер воспоминаий


С Шурой Березиным.. Где-то в районе Эльбруса... Вдали виднеется Ушба...

воскресенье, 23 ноября 2014 г.

Коротко. Про лямбды и копирующие конструкторы в C++

http://programmingmindstream.blogspot.ru/2014/11/delphi_21.html?showComment=1416695404969#c6071212647984907418

Есть "поучительная история" про "лямбды".

Про C++.

https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

https://ru.wikipedia.org/wiki/C%2B%2B11#.D0.9B.D1.8F.D0.BC.D0.B1.D0.B4.D0.B0-.D1.84.D1.83.D0.BD.D0.BA.D1.86.D0.B8.D0.B8_.D0.B8_.D0.B2.D1.8B.D1.80.D0.B0.D0.B6.D0.B5.D0.BD.D0.B8.D1.8F

Процитирую:

"

Лямбда-функции и выражения

В стандартном C++, например, при использовании алгоритмов стандартной библиотеки C++ sort и find, часто возникает потребность в определении функций-предикатов рядом с местом, где осуществляется вызов этого алгоритма. В языке существует только один механизм для этого: возможность определить класс функтора (передача экземпляра класса, определенного внутри функции, в алгоритмы запрещена (Meyers, Effective STL)). Зачастую данный способ является слишком избыточным и многословным и лишь затрудняет чтение кода. Кроме того, стандартные правила C++ для классов, определённых в функциях, не позволяют использовать их в шаблонах и таким образом делают их применение невозможным.
Очевидным решением проблемы явилось разрешение определения лямбда-выражений и лямбда-функций в C++11. Лямбда-функция определяется следующим образом:
[](int x, int y) { return x + y; }
Тип возвращаемого значения этой безымянной функции вычисляется как decltype(x+y). Тип возвращаемого значения может быть опущен только в том случае, если лямбда-функция представлена в форме return expression. Это ограничивает размер лямбда-функции до одного выражения.
Тип возвращаемого значения может быть указан явно, например:
[](int x, int y) -> int { int z = x + y; return z; }
В этом примере создаётся временная переменная z для хранения промежуточного значения. Как и в нормальных функциях, это промежуточное значение не сохраняется между вызовами.
Тип возвращаемого значения может быть полностью опущен, если функция не возвращает значения (то есть тип возвращаемого значения — void)
Также возможно использование ссылок на переменные, определённые в той же области видимости, что и лямбда-функция. Набор таких переменных обычно называют замыканием. Замыкания определяются и используются следующим образом:"

И ещё:

"std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&total](int x) {
  total += x;
});
std::cout << total;
Это отобразит сумму всех элементов в списке. Переменная total хранится как часть замыкания лямбда-функции. Так как она ссылается на стековую переменную total, она может менять её значение.
Переменные замыкания для локальных переменных могут быть также определены без использования символа ссылки &, что означает, что функция будет копировать значение. Это вынуждает пользователя заявлять о намерении сослаться на локальную переменную или скопировать её."

Много букв процитировал.

А сам напишу коротко:

Пусть есть класс:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- обратим внимание - тут НЕТ нормального копирующего конструктора и оператора присваивания.

Теперь пусть есть код:

 A x ("Hello world");
 char * y;
 std::vector<int> someList = {1};
 // - просто чтобы вектор был не пуст, хотя это и не так важно
 std::for_each(someList.begin(), someList.end(), [](int anItem) {
   y := x.getString();
   // - типа просто получили ссылку на строку и ничего с ней не делаем
 });

- казалось бы - "ничего такого" - мы тут не сделали.

Просто один раз дёрнули подитеративную функцию.

Однако мы получим AV.

Почему?

А потому, что по-умолчанию лямбды "захватывают" все внешние переменные - по значению.

Я ведь - не зря всё это выше цитировал.

Что тут происходит?

Обратим внимание на строчку:

 y := x.getString();

- казалось бы тут "ничего такого".

Однако - это не так.

Если эту строчку закомментировать, то AV - пропадёт.

В чём же дело?

А вот в чём.

Мы ведь зовём x.getString() внутри анонимного метода.

И что происходит?

А то, что за x.getString() - скрывается copy_of_x.getString(), которая получается при создании анонимного метода.

А как получается?

По значению. Читаем выше.

И как по значению? Через вызов копирующего конструктора, который у нас - умолчательный.

Который просто копирует поля класса.

Соответственно поле m_String просто копируется при получении copy_of_x из x.

А освобождается эта строка ДВА раза - для x и для copy_of_x.

Откуда AV - теперь понятно?

Теперь что же делать?

Самый простой вариант это:

 A x ("Hello world");
 char * y;
 std::vector<int> someList = {1};
 // - просто чтобы вектор был не пуст, хотя это и не так важно
 std::for_each(someList.begin(), someList.end(), [&x](int anItem) {
   y := x.getString();
   // - типа просто получили ссылку на строку и ничего с ней не делаем
 });

- что мы тут сделали?

Мы написали - [&x] вместо [].

Этим мы сказали, что x явно передаётся по ссылке, а не по значению.

И тогда - копия сниматься - не будет.

Ну или можно определить копирующий конструктор:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 A (const A & anOther)
 {
  m_String = strnew(anOther.m_String);
  // - получили копию строки
 }

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

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

И тогда будут работать оба варианта вызова - и [] и [&x].

Теперь как сделать так, чтобы вызов по значению с [] был невозможен?

Ну пусть природа нашего класса такова, что его нельзя копировать.

Как это сделать?

Ну в "обычном C++" можно объявить приватный пустой копирующий конструктор:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

private:
 A (const A & anOther);

public:
 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- и тогда на "факт копирования" ругнётся компилятор. Скажет "неопределённый копирующий конструктор".

В C++11 можно сделать более "красиво" - удалить копирующий конструктор по-умолчанию:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 A (const A & anOther) = delete;

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- результат будет тем же. На "факт копирования" ругнётся компилятор.

Ну вот собственно пока и всё.

Ни на что не претендую.

Нет. Ещё не всё...

Про Objective-C - забыл.

Там можно написать так:

 A x ("Hello world");
 char * y;
 [m_HiddenTextIndex enumerateKeysAndObjectsUsingBlock:^(id docId, id data, BOOL *) {
  y := x.getString();
  // - типа просто получили ссылку на строку и ничего с ней не делаем
 }];

-- ^(id docId, id data, BOOL *) - это тоже анонимная функция.

И внутри неё используется x.getString(), которая тоже copy_of_x.getString().

И этой "лямбде" нет средств указать, что передаём по значению.

Ну кроме модификатора __block. Но там тоже - "есть нюансы".

И посему - для такого кода можно только или запретить копирующий конструктор, или определить его правильно.

Вот - теперь всё.

суббота, 22 ноября 2014 г.

Коротко. Про STL под iOS/MacOS

Коротко. Про STL под iOS/MacOS.

Ни на что не претендую.

Несколько по мотивам:
Коротко. Об оптимизации потребления памяти под xCode. Затравочка
Коротко. Сегодня опять рефакторил свой код под xCode

Что хочу сказать?

Я - для себя лично - очередной раз убедился, что "лобовое" использование STL не годиться к написанию программ для работы в режиме 24х7.

Ну на тех платформах, что я знаю.

А именно:
Windows.
MacOs.
iOS.

STL - о конечно - хорош. Более чем.

Лучшей контейнерной библиотеки - я не видел.

Но! Не готов он в "среднем по больнице" к 24х7.

Ибо постоянно перераспределяет и копирует объекты.

И менеджеры памяти "в среднем по больнице" - этого не любят.

Ну и про "кривые руки" - не надо забывать.

Например когда написано:

std::string theValue = theContainer.getRef();
// - тут копирование данных

А не:

const std::string & theValue = theContainer.getRef();
// - тут нет копирования данных

Я вот провёл "несколько пасов руками" и достаточно оптимизировал.

Что ещё могу сказать?

Это то, что Obj-C объекты "в среднем по больнице" реализованы "эффективнее", чем C++ объекты.

За счёт View, подсчёта ссылок и кластеризации.

Например замена std::string на NSString (именно NSString, а не NSMutableString) дала ощутимый прирост как производительности, так и экономию памяти "при прочих равных".

Ну вот как-то так.

Ни на что не претендую.

Если у кого-то вдруг есть конкретные вопросы - пишите. Лучше в личку - lulinalex@gmail.com.

Написано несколько сумбурно.

Но сегодня - суббота, а я наверное часов 11-ть отработал. С непростыми задачами.

Да - и ещё я забыл написать, что во многом спасает использование STL со своими специализированными аллокаторами.

пятница, 21 ноября 2014 г.

Ссылка. Мотивационное

Мне как бывшему геймеру, да ещё и в стратегические игры очень понравилось. Эффект от прочтения окался примерно такой же как от книги "Алхимик".

"Реальная жизнь – это игра, в которую играют в буквальном смысле все. Но игра эта непростая. И вот ее суть."

вторник, 18 ноября 2014 г.

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

http://eao197.blogspot.ru/2014/11/progcflame.html?m=1

Не скажу, чтобы я был согласен с автором, но само по себе - забавно. И по-моему - стоит прочтения. Естественно не в плане "разжигания".

четверг, 13 ноября 2014 г.

MindStream. How we develop software for FireMonkey. Part 3. Firemonkey + DUnit

Original post in Russian http://habrahabr.ru/post/241301/

Table of contents

In this post, I want to describe the process of transferring VCL code to FireMonkey. As far as I remember DUnit was a part of Delphi since Delphi 2009. It has been written in the early days of VCL and although it allows you to test code written for FireMonkey (thanks to the console output), it does not have GUIRunner, to which many of us are accustomed to, because it's very fast and easy, for instance, to disable those tests that we do not want to run.  

For those who are not familiar with DUnit, to create a DUnit project you should use File-> New-> Other-> Unit Test-> TestProject. Next, you should choose GUI or console version. After these simple manipulations, you will have a new project which will look something like (GUI, at least for my XE7) the following:
program Project1Tests;
{

   Delphi DUnit Test Project
  -------------------------
  This project contains the DUnit test framework and the GUI/Console test runners.
  Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options
  to use the console test runner.  Otherwise the GUI test runner will be used by
  default.

 }

 {$IFDEF CONSOLE_TESTRUNNER}
{$APPTYPE CONSOLE}
{$ENDIF}

 uses
  DUnitTestRunner,
  TestUnit1 in 'TestUnit1.pas',
  Unit1 in '..\DUnit.VCL\Unit1.pas';

 {$R *.RES}

 begin
  DUnitTestRunner.RunRegisteredTests;
end.
Then you have to add a TestCase (File-> New-> Other-> Unit Test-> TestCase). And the result will look like:
unit TestUnit1;
{

   Delphi DUnit Test Case
  ----------------------
  This unit contains a skeleton test case class generated by the Test Case Wizard.
  Modify the generated code to correctly setup and call the methods from the unit 
  being tested.

 }

 interface

 uses
  TestFramework, System.SysUtils, Vcl.Graphics, Winapi.Windows, System.Variants,
  System.Classes, Vcl.Dialogs, Vcl.Controls, Vcl.Forms, Winapi.Messages, Unit1;

 type
  // Test methods for class TForm1

   TestTForm1 = class(TTestCase)
  strict private
    FForm1: TForm1;
  public
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestDoIt;
  end;

 implementation

 procedure TestTForm1.SetUp;
begin
  FForm1 := TForm1.Create;
end;

 procedure TestTForm1.TearDown;
begin
  FForm1.Free;
  FForm1 := nil;
end;

 procedure TestTForm1.TestDoIt;
var
  ReturnValue: Integer;
begin
  ReturnValue := FForm1.DoIt;
  // TODO: Validate method results
end;

 initialization
  // Register any test cases with the test runner
  RegisterTest(TestTForm1.Suite);
end.
In general, my example shows how you can easily add unit-testing even to Delphi 7. All we need is to call DUnitTestRunner.RunRegisteredTests and add new files with TestCases to the project. In more detail unit-testing, using DUnit, is considered here. Implementing GUIRunner for FireMonkey, I decided to repeat the guys who did the original DUnit. The first problem (I will not even tell that TTreeNode and TTreeViewItem classes are not compatible) that I encountered was here:
type
  TfmGUITestRunner = class(TForm)
  ...
  protected
    FSuite: ITest;
    procedure SetSuite(Value: ITest);  
  ...  
  public
    property Suite: ITest read FSuite write SetSuite;
  end;  
 
procedure RunTestModeless(aTest: ITest);
var
  l_GUI: TfmGUITestRunner;
begin
  Application.CreateForm(TfmGUITestRunner, l_GUI);
  l_GUI.Suite := aTest;
  l_GUI.Show;
end;

 procedure TfmGUITestRunner.SetSuite(Value: ITest);
begin
  FSuite := Value; // AV here
 
  if FSuite <> nil then
    InitTree;
end;
In FireMonkey Application.CreateForm method does not create a form. Yes, it is oddly enough. Here is another link about that. It does not do what it says it does! I was, to be honest, a bit shocked by the fact that the method called "CreateForm" does not create it. To solve this issue I create forms explicitly (l_GUI := TfmGUITestRunner.Create (nil);) and go further. Now we need to build a tests tree based on TestCases we added for testing. If you notice, the construction of the form starts in RunRegisteredTestsModeless method.
procedure RunRegisteredTestsModeless;
begin
  RunTestModeless(registeredTests)
end;
I decided not to put this method into a separate module, as DUnit developers did, thus to use fmGUITestRunner, you must specify its module in the project code and actually call the appropriate method. In my case, the code is as follows:
program FMX.DUnit;
uses
  FMX.Forms,
  // GUI Runner
  u_fmGUITestRunner in 'u_fmGUITestRunner.pas' {fmGUITestRunner},
  // Tests
  u_FirstTest in 'u_FirstTest.pas',
  u_TCounter in 'u_TCounter.pas',
  u_SecondTest in 'u_SecondTest.pas';

 {$R *.res}

 begin
 Application.Initialize;
 u_fmGUITestRunner.RunRegisteredTestsModeless;
 Application.Run;
end.
The attentive reader will notice that we have not added any registeredTests, and it is never specified what kind of tests will be added. RegisteredTests is a global method of TestFrameWork, which is connected to our form; it returns __TestRegistry: ITestSuite (global variable); The way how TestRegistry gets TestCases is out of the scope of this article. Moreover, that work has been done by DUnit developer. However, if readers have an interest in this topic, I'll reply to the comments. So, back to the tree. A method to initialize the tree:
procedure TfmGUITestRunner.InitTree;
begin
  FTests.Clear;
  FillTestTree(Suite);
  TestTree.ExpandAll;
end;
FTests is a list of interface objects, which will store a list of our tests. FillTestTree is overloaded because we do not know if we are working with a root of the tree or with an ordinary node:
...
    procedure FillTestTree(aTest: ITest); overload;
    procedure FillTestTree(aRootNode: TTreeViewItem; aTest: ITest); overload;
...
procedure TfmGUITestRunner.FillTestTree(aRootNode: TTreeViewItem; aTest: ITest);
var
  l_TestTests: IInterfaceList;
  l_Index: Integer;
  l_TreeViewItem: TTreeViewItem;
begin
  if aTest = nil then
    Exit;

   l_TreeViewItem := TTreeViewItem.Create(self);
  l_TreeViewItem.IsChecked := True;

   // Add Index to tag property
  l_TreeViewItem.Tag := FTests.Add(aTest);
  l_TreeViewItem.Text := aTest.Name;

  if aRootNode = nil then
    TestTree.AddObject(l_TreeViewItem)
  else
    aRootNode.AddObject(l_TreeViewItem);

  l_TestTests := aTest.Tests;
  for l_Index := 0 to l_TestTests.Count - 1 do
    FillTestTree(l_TreeViewItem, l_TestTests[l_Index] as ITest);
end;
As you can see, in this method, we not only fill the tree, but also add information to each node, which of the tests corresponds to it. In order to get a test by a given node, let’s implement a method named NodeToTest:
function TfmGUITestRunner.NodeToTest(aNode: TTreeViewItem): ITest;
var
  l_Index: Integer;
begin
  assert(aNode.Tag >= 0);
  l_Index := aNode.Tag;
  Result := FTests[l_Index] as ITest;
end;
Now let's add some “knowledge” to a test. There is a variable GUIObject (TObject) in each test. And we will call SetupGUINodes method in FormShow.
procedure TfmGUITestRunner.SetupGUINodes(aNode: TTreeViewItem);
var
  l_Test: ITest;
  l_Index: Integer;
begin
  for l_Index := 0 to Pred(aNode.Count) do
  begin
    // Give test
    l_Test := NodeToTest(aNode.Items[l_Index]);
    assert(assigned(l_Test));
    // associate node to test
    l_Test.GUIObject := aNode.Items[l_Index];
    SetupGUINodes(aNode.Items[l_Index]);
  end;
end;
In order to get a node corresponding to a test let’s write a method
function TfmGUITestRunner.TestToNode(test: ITest): TTreeViewItem;
begin
  assert(assigned(test));

   Result := test.GUIObject as TTreeViewItem;

   assert(assigned(Result));
end;
My senior colleague and I don’t like the way I connect the tests and the tree. I understand why DUnit developer done this that way. DUnit has been developed long ago; Generics were not available. We will change it in the future. At the end of this post, I will write about our upcoming improvements and wishes. So our tree is constructed, all the tests are inside FTests. Tests and the tree are connected. It's time to run the tests and interpret the results. To ensure that the form can do it, let's add to the form an implementation of the ITestListener interface described in TestFrameWork:
  { ITestListeners get notified of testing events.
    See TTestResult.AddListener()
  }
  ITestListener = interface(IStatusListener)
    ['{114185BC-B36B-4C68-BDAB-273DBD450F72}']

     procedure TestingStarts;
    procedure StartTest(test: ITest);

     procedure AddSuccess(test: ITest);
    procedure AddError(error: TTestFailure);
    procedure AddFailure(Failure: TTestFailure);

     procedure EndTest(test: ITest);
    procedure TestingEnds(testResult :TTestResult);

     function  ShouldRunTest(test :ITest):Boolean;
  end;
Let’s add these methods to the class interface and implement them:
procedure TfmGUITestRunner.TestingStarts;
begin
  FTotalTime := 0;
end;

 procedure TfmGUITestRunner.StartTest(aTest: ITest);
var
  l_Node: TTreeViewItem;
begin
  assert(assigned(TestResult));
  assert(assigned(aTest));

   l_Node := TestToNode(aTest);

   assert(assigned(l_Node));
end;

 procedure TfmGUITestRunner.AddSuccess(aTest: ITest);
begin
  assert(assigned(aTest));
  SetTreeNodeFont(TestToNode(aTest), c_ColorOk)
end;

 procedure TfmGUITestRunner.AddError(aFailure: TTestFailure);
var
  l_ListViewItem: TListViewItem;
begin
  SetTreeNodeFont(TestToNode(aFailure.failedTest), c_ColorError);

   l_ListViewItem := AddFailureNode(aFailure);
end;

 procedure TfmGUITestRunner.AddFailure(aFailure: TTestFailure);
var
  l_ListViewItem: TListViewItem;
begin
  SetTreeNodeFont(TestToNode(aFailure.failedTest), c_ColorFailure);

   l_ListViewItem := AddFailureNode(aFailure);
end;

 procedure TfmGUITestRunner.EndTest(test: ITest);
begin
  // comment this assert because if not comment, we don't have any result 
  // assert(False);
end;

 procedure TfmGUITestRunner.TestingEnds(aTestResult: TTestResult);
begin
  FTotalTime := aTestResult.TotalTime;
end;

 function TfmGUITestRunner.ShouldRunTest(aTest: ITest): Boolean;
var
  l_Test: ITest;
begin
  l_Test := aTest;
  Result := l_Test.Enabled
end;
There’s nothing special here to explain. Although if you have some questions, I will give a detailed answer. In the original DUnitRunner after receiving a test result, it changes a picture of the corresponding tree node. I decided not to deal with pictures but change the FontColor and FontStyle for each node. It looks like it takes a minute, but I spent a couple of hours, having dug through all the documentation
procedure TfmGUITestRunner.SetTreeNodeFont(aNode: TTreeViewItem;
  aColor: TAlphaColor);
begin
  // Set style settings
  aNode.StyledSettings := aNode.StyledSettings - [TStyledSetting.ssFontColor, TStyledSetting.ssStyle];
  aNode.Font.Style := [TFontStyle.fsBold];
  aNode.FontColor := aColor;
end;
To output results, we will use ListView. TListView in FireMonkey is fully optimized for mobile applications but lost his wonderful Columns property. AddFailureNode is a method to add failures:
function TfmGUITestRunner.AddFailureNode(aFailure: TTestFailure): TListViewItem;
var
  l_Item: TListViewItem;
  l_Node: TTreeViewItem;
begin
  assert(assigned(aFailure));
  l_Item := lvFailureListView.Items.Add;

   l_Item.Text := aFailure.failedTest.Name + '; ' + 
                 aFailure.thrownExceptionName + '; ' + 
                 aFailure.thrownExceptionMessage + '; ' + 
                 aFailure.LocationInfo + '; ' + 
                 aFailure.AddressInfo + '; ' + 
                 aFailure.StackTrace;

   l_Node := TestToNode(aFailure.failedTest);
  while l_Node <> nil do
  begin
    l_Node.Expand;
    l_Node := l_Node.ParentItem;
  end;

   Result := l_Item;
end;
It's time to run our tests; we will add a button and a launch method:
procedure TfmGUITestRunner.btRunAllTestClick(Sender: TObject);
begin
  if Suite = nil then
    Exit;

   ClearResult;
  RunTheTest(Suite);
end;

 procedure TfmGUITestRunner.RunTheTest(aTest: ITest);
begin
  TestResult := TTestResult.Create;
  try
    TestResult.addListener(self);
    aTest.run(TestResult);
  finally
    FreeAndNil(FTestResult);
  end;
end;
After running our Runner and clicking the button, we will see the following: image

The last thing we need to do is to handle user actions during the change of node state:
procedure TfmGUITestRunner.TestTreeChangeCheck(Sender: TObject);
begin
  SetNodeEnabled(Sender as TTreeViewItem, (Sender as TTreeViewItem).IsChecked);
end;

 procedure TfmGUITestRunner.SetNodeEnabled(aNode: TTreeViewItem;
  aValue: Boolean);
var
  l_Test: ITest;
begin
  l_Test := NodeToTest(aNode);
  if l_Test <> nil then
    l_Test.Enabled := aValue;
end;
Changing checkboxes state for some nodes: image
The test code which I have actually used:
unit u_SecondTest;

 interface

 uses
  TestFrameWork;

 type
  TSecondTest = class(TTestCase)
  published
    procedure DoIt;
    procedure OtherDoIt;
    procedure ErrorTest;
    procedure SecondErrorTest;
  end; // TFirstTest

 implementation

 procedure TSecondTest.DoIt;
begin
  Check(true);
end;

 procedure TSecondTest.ErrorTest;
begin
  raise ExceptionClass.Create('Error Message');
end;

 procedure TSecondTest.OtherDoIt;
begin
  Check(true);
end;

 procedure TSecondTest.SecondErrorTest;
begin
  Check(False);
end;

 initialization

 TestFrameWork.RegisterTest(TSecondTest.Suite);
end.
To summarize: at this moment, we have a fully working application for testing FireMonkey code using the usual GUIRunner. The project is open, so everyone can use. Plans for the future: Write a method to traverse the tree which will get a lambda function. The tree has to be around permanently, but the steps are different for each branch, so I think lambda function seems to be appropriate. Comments and suggestions from my senior colleague: To rebuild Tests and Nodes connection with TDictionary<TTreeViewItem, ITest> docwiki.embarcadero.com/Libraries/XE7/en/System.Generics.Collections.TDictionary To add graphic indication of tests execution process. Add buttons: select all, unselect all, and so on, and the output of test results (execution time, the number of successful and failed tests, and so on). To add the Decorator pattern to get rid of the GUIObject. In the near future, we will start to cover our MindStream project with unit-tests, and also step by little step bring improvements to the Runner. Thanks to all who have read to the end. Comments and criticism are as always welcome in the comments. Link to the repository. p.s. The project is located at MindStream\FMX.DUnit path. Links that were useful: http://sourceforge.net/p/radstudiodemos/code/HEAD/tree/branches/RadStudio_XE5_Update/FireMonkey/Delphi/ http://fire-monkey.ru/ http://18delphi.blogspot.ru/ http://www.gunsmoker.ru/ And of course http://docwiki.embarcadero.com/RADStudio/XE7/en/Main_Page

P.S. Thanks for translating to Roman Yankovsky.

среда, 12 ноября 2014 г.

Offtopic. Хотел уже пойти спать. Но не могу не написать

Госуслуги это - "что-то"с "чем-то".

"Не могу записать детей к врачу онлайн.Ни через pgu ни через gosuslugi. Ошибка сервера. Зайдите попозже. И так уже несколько часов. Руки блин поотрывал бы. И голову. И кое что ещё.
Придётся завтра идти пешком и пытаться записывать детей до пятницы. Ибо до воскресенья - "кровь из носу" надо взять справку. Дети уезжают отдыхать.А про справку - только сегодня сообщили.
Эх блин.
Интересно - справка из платной клиники бывает? Она пансионату покатит? Зачем им вообще справка?
"Формальные процедуры - убивают все идеи".
http://programmingmindstream.blogspot.ru/…/blog-post_46.html"

"
  • XXX: А чем платная поликлиника отличается? Лицензия то есть...
  • Александр Люлин Не знаю? Никогда не пробовал там брать справку. Отличается лишь тем, что там не нужна запись. Ну можно найти - где "срочно". Заплатить денег. И получить услугу. Раз уж предоставители бесплатных услуг такие криворукие.
"

"
  • Александр Люлин А ещё эти идиоты не умеют запоминать реквизиты полисов, например членов семьи и детей. Приходится КАЖДЫЙ раз вводить ЗАНОВО. А не выбирать из списка. Криворукие.
  • Александр Люлин И кстати баг-репорты - им тоже не отправляются.
  • Александр Люлин У меня тут коллег смог записаться в ГАИ лишь тогда,когда он распарсил SQL ответи ПОНЯЛ, что КОГДА-ТО в профиле не указал свой пол. И запрос ругался - Invalid Data. БЕЗ указания - ГДЕ Invalid. Уроды криворукие.
"

"
  • Александр Люлин За ТАКИЕ ошибки - мне лично весь мозг группа качества проедает. И они -правы.
  • Александр Люлин "У них вроде на всем портале общий механизм — выдели и нажми ctrl-enter, неделю назад работало..." - "ошибка сервера" или "переход на главную страницу"
"

"Александр Люлин "Еще есть телефонная запись вообще то..." - только не в 8-9-10 вечера. А "сегодня" не записался, значит и "послезавтра не попадёшь".
Только что · Нравится"

Ну и "на закуску" - "Лирическое отступление о "надо"..."

Кстати к Beeline'у - добавился Reiffeisen банк, где у меня есть счёт. Они видимо слили свою клиентскую базу страховой компании. И звонят - "мы Reiffeisen банк, мы хотим предложить вам страхование". Я уже РУССКИМ языком им СКАЗАЛ - "МНЕ НЕ ИНТЕРЕСНО". ВСЁ РАВНО - звонят.

Вот вам и "Австрия". "МЕНЕДЖЕРЫ" блин...

Возвращаясь к "госуслугам":

Да! Зачем понадобились ДВА принципиально РАЗНЫХ интерфейса - htt://pgu.mos.ru и http://gosuslugi.ru ? Чтобы в ДВА РАЗА больше денег освоить?

Ну и потом люди пишут в комментариях - "ну некогда тесты писать, да и не надо"... :-(