Архив рубрики: C Sharp

Почему C# хуже C++?

С момента моего ознакомительного исследования C# прошло полтора года. Поработав с языком, я убедился в превосходстве C++ над ним. Когда я говорю об этом, мои оппоненты часто начинают приводить доводы, касательно того, как хорош .NET или, что архитектура CLR накладывает ограничения на язык. Ни тот, ни другой аргументы не могут оправдать C#, так как язык -- это то, что программисты видят каждый день, а CLR скрыт от глаз и не должен оказывать влияния на инструмент разработки, как архитектура процессора не оказывает влияния на C++. В этой статье я не буду обсуждать стандартную библиотеку и рантайм, речь пойдёт только о языке. Не затягивая со вступлением, начну с самых основ.
Якщо вам потрібен якісний сайт або портал, то рекомендуємо звернутися у компанію  web-ukraine, вони допоможуть усе зробити по вашим бажанням, створення сайтів, підтримка сайтів, реклама сайтів все це входить у вартість послуг.

ООП ради ООП

Программируя долгое время на C++, я не задумывался о минусах ОО подхода. Один мой знакомый постоянно спорил со мной, утверждая, что ООП -- это зло. Тогда я не понимал истинных причин его выводов. Лишь начав программировать на C#, я осознал всю несостоятельность объектно-ориентированной парадигмы. Дело в том, что пока я писал на C++, я активно пользовался процедурной парадигмой и ООП лишь помогало там, где оно имело место. В C# (как впрочем и в Java) объектно-ориентированная парадигма вытеснила все другие. В итоге в C# мы имеем множество статических классов-Helper`ов и Utility. Гипертрофия ООП в C# приводит к тому, что классы содержат невероятно избыточное количество данных для настройки своего поведения и поведения своих потомков. Подробнее о недостатках ООП подхода можно почитать здесь. Приведу одну цитату:

Единственный способ сделать повторно используемые бизнес компоненты на этом уровне — сделать их сверх-настраиваемыми путём добавления таких штук как движки правил и встраиваемые языки. Такую модель я бы не стал называть моделью компонента. Скорее моделью очередного экземпляра bloatware. Обещание о повторном использовании ушло, люди либо покупали огромные bloatware системы(неудачники), или разрабатывали свои специальные бизнес объекты в своих собственных проектах.

Кроме того, Вы можете ознакомиться со знаменитой статьёй Ричарда Гэбриэла "Объектная парадигма провалилась" и её триумфальное возрождением "Почему объектно-ориентированное программирование провалилось?". Советую обратить внимание ещё и на эту статью. Подводя итог вышеперечисленным статьям, можно сказать, что концепция ООП не справилась с возложенными на неё ожиданиями, вместо этого она усложнила разработку и архитектуру приложений. C++ же не настаивает на использовании ООП и позволяет получить от него максимум выгоды, используя там, где это оправдано. Доклад Джека Дидриха довольно точно описывает моё отношение к ООП: не нужно писать класс, если можно написать функцию. Исходя из того, что C# ничего, кроме ООП не предлагает, в дальнейшем я буду рассматривать C# и C++ с точки зрения объектно-ориентированной парадигмы.

Наследование

Наследование в C++ -- это одна из самых широких возможностей. C++ позволяет использовать один из нижеприведённых вариантов или любую их комбинацию:

  • Наследование интерфейса;
  • Наследование реализации;
  • Множественное наследование;
  • Виртуальное наследование.

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

class Clock
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone
{
public:
    PhoneStream * call(const std::string & number);
};

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

class CellPhone : public Clock, public Phone { };

Вместо одной строки кода, на C# придётся написать

class CellPhone {
    Phone phone = new Phone();
    Clock clock = new Clock();

    public Phone Phone {
        get { return phone; }
    }

    public Clock Clock {
        get { return clock; }
    }
}

При такой реализации клиенты этого класса будут вынуждены углубить связность с его внутренней реализацией, что противоречит закону Деметры. Чтобы избежать связности можно реализовать все методы классов Clock и Phone внутри класса CellPhone, но это имеет смысл только при условии, что указанные классы имеют интерфейсы и используются только через них. Теперь усложним задачу. Каждый класс, Clock и Phone, будут унаследованы от класса Device (устройство).

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

В этом коде есть одна, не слишком очевидная, проблема. Объекты класса CellPhone будут иметь два экземпляра класса Device: один от Clock, другой от Phone. Для таких случаев (которые получили название ромбовидное наследование) в C++ введён специальный тип наследования -- виртуальное наследование.

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public virtual Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public virtual Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

Теперь объекты CellPhone будут иметь один экземпляр класса Device. C# не предоставляет подобной возможности. В агрегированных объектах у Вас всегда будут храниться два разных объекта класса Device, бездарно расходуя память. Подробно о пользе и вреде множественного наследования  Вы можете прочитать в книге Скотта Майерса "Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов" в правиле 42: "Продумывайте подход к использованию множественного наследования". Более реальный пример множественного наследования  -- реализация паттерна проектирования Observer (наблюдатель).

#include <iostream>
#include <set>

template<typename T>
class Observable
{
protected:
    Observable() { }
    virtual ~Observable() { }

public:
    bool addObserver(T * observer)
    {
        return m_observers.insert(observer).second;
    }

    bool removeObserver(T * observer)
    {
        return m_observers.erase(observer);
    }

protected:
    std::set<T *> m_observers;
};

class Worker
{
public:
    virtual ~Worker() { }
    virtual void start() = 0;
    virtual void stop() = 0;
};

class WorkerObserver
{
public:
    virtual ~WorkerObserver() { }
    virtual void onStart() = 0;
    virtual void onStop() = 0;
};

class ObservableWorker : public Observable<WorkerObserver>, public Worker
{
public:
    virtual void start()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStart();
        }
    }

    virtual void stop()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStop();
        }
    }
};

class WorkerLogger : public WorkerObserver
{
public:
    virtual void onStart()
    {
        std::cout << "Started" << std::endl;
    }

    virtual void onStop()
    {
        std::cout << "Stoped" << std::endl;
    }
};


int main()
{
    ObservableWorker worker;
    WorkerLogger logger;
    worker.addObserver(&logger);
    worker.start();
    worker.stop();
    std::cin.get();
    return 0;
}

В данном подходе класс ObservableWorker является одновременно и Worker`ом и Observable`ом. Без множественного наследования пришлось бы выбирать между родителями, и выбор был бы в пользу Worker`а, а методы addObserver и removeObserver пришлось бы реализовавать в каждом классе. Кроме упомянутых методов, класс Observable мог бы содержать дополнительные методы работы с коллекцией наблюдателей, но для краткости я не стал раздувать код примера.

Ещё один вид наследования, который оказался за бортом C# -- наследование реализации без интерфейса или, проще говоря, приватное наследование. Этот вид наследования не делает унаследованный класс потомком родителя и не может участвовать в его полиморфных операциях. По большому счёту, закрытое наследование мало отличается от реализации класса посредством агрегации объекта некоторого другого класса (того, от которого производится наследование). Но в закрытом наследовании есть один большой плюс: оно предоставляет доступ ко всем защищённым членам базового класса. Один из вариантов использования закрытого наследования приведён в книге Скотта Майерса "Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов" в правиле 42: "Продумывайте подход к использованию закрытого наследования". Этот пример довольно интересен, но является специфичным для C++, поэтому тут я его приводить не буду.

Инкапсуляция

Один из самых любимых мною, да и всеми C++ программистами, паттерн Private Implementation (например, библиотека Qt очень активно использует этот паттерн) не применим в C# вообще. Причиной этому является открытость всех модулей. В C++, желая скрыть детали имплементации класса, мы можем предварительно объявить в заголовочном файле приватный внутренний класс и указатель на него, а сам интерфейс и реализацию перенести в *.cpp модуль. Тем самым мы скрываем все переменные и дополнительные, вспомогательные методы, которые мы используем, от посторонних глаз. Интерфейс остаётся чистым и понятным при реализации любой сложности. В описанной стратегии очень сложно достучаться до деталей приватной имплементации. В C# при помощи reflection можно вызвать любой приватный метод и изменить приватное состояние объекта всего в одну-две строки кода. Но этот соблазн ещё выше с учётом того, что все внутренности класса находятся в одном модуле.

Объявление синонимов типа

Создатели C# посчитали, что оператор typedef им не нужен. В C++ оператор typedef служит для объявления синонимов типов. Для чего это нужно? К примеру, Вы используете в качестве параметра метода такой тип

Tuple<Dictionary<string, List<MyObject>>, bool>

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

typedef Tuple<Dictionary<string, List<MyObject>>, bool> MyType;

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

#include <iostream>
#include <vector>

struct Letter
{
    typedef char ValueType;
    char value;
};

struct Number
{
    typedef int ValueType;
    int value;
};

template<typename Element>
class Container
{
public:
    typedef typename Element::ValueType  ElementValueType;

    void add(ElementValueType value)
    {
        Element element;
        element.value = value;
        m_elements.push_back(element);
    }

    void print()
    {
        for(typename std::vector<Element>::const_iterator it = m_elements.begin();
            m_elements.end() != it; ++it)
        {
            std::cout << it->value << " ";
        }
    }
    
private:
    std::vector<Element> m_elements;
};


void testLetter()
{
    Container<Letter> container;
    container.add('a');
    container.add('b');
    container.add('c');
    std::cout << "Letters: ";
    container.print();
    std::cout << std::endl;
}

void testNumber()
{
    Container<Number> container;
    container.add(1);
    container.add(2);
    container.add(3);
    std::cout << "Numbers: ";
    container.print();
    std::cout << std::endl;
}

int main()
{
    testLetter();
    testNumber();
    std::cin.get();
}
Letters: a b c
Numbers: 1 2 3

Класс Container получает из типа шаблона тип значения, которое хранится в некотором другом классе и которое доступно из переменной value. Далее класс Container может оперировать этим типом, в том числе и в параметрах методов. Typedef`ы типов могут быть вложены и тогда классы Number и Letter могут стать одним шаблоном. Работают такие конструкции во время компиляции, что обеспечивает почти полную защиту от ошибок. Более сложные примеры с большой вложенностью Вы сможете найти в заголовочных файлах контейнеров (std::vector, std::map итд) стандартной библиотеки шаблонов (STL) С++.

Обобщения

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

class Connection
{
public:
    virtual ~Connection() { }
    virtual bool connect(const std::string & address, short port) = 0;
    virtual void disconnect() = 0;
    virtual bool isConnected() const = 0;
    virtual void send(char * buffer, size_t size) = 0;
    virtual size_t recv(char * buffer, size_t size) = 0;
};

И от него унаследованы два (или более) конкретных класса: TcpConnection и SshConnection

class TcpConnection : public Connection
{
public:
    inline TcpConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

TcpConnection::TcpConnection() :
    m_is_connected(false)
{
}

bool TcpConnection::connect(const std::string & address, short port)
{
    std::cout << "TcpConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void TcpConnection::disconnect()
{
    std::cout << "TcpConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool TcpConnection::isConnected() const
{
    std::cout << "TcpConnection::isConnected" << std::endl;
    return m_is_connected;
}

void TcpConnection::send(char * buffer, size_t size)
{
    std::cout << "TcpConnection::send" << std::endl;
}

size_t TcpConnection::recv(char * buffer, size_t size)
{
    std::cout << "TcpConnection::recv" << std::endl;
    return size;
}
class SshConnection : public Connection
{
public:
    inline SshConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

SshConnection::SshConnection() :
    m_is_connected(false)
{
}

bool SshConnection::connect(const std::string & address, short port)
{
    std::cout << "SshConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void SshConnection::disconnect()
{
    std::cout << "SshConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool SshConnection::isConnected() const
{
    std::cout << "SshConnection::isConnected" << std::endl;
    return m_is_connected;
}

void SshConnection::send(char * buffer, size_t size)
{
    std::cout << "SshConnection::send" << std::endl;
}

size_t SshConnection::recv(char * buffer, size_t size)
{
    std::cout << "SshConnection::recv" << std::endl;
    return size;
}

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

template<typename T>
class ConnectionAdapter : public T
{
public:
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
};

template<typename T>
bool ConnectionAdapter<T>::connect(const std::string & address, short port)
{
    bool connected = T::connect(address, port);
    std::cout << "Log: " << (connected ? "Connected" : "Connection failed") << std::endl;
    return connected;
}

template<typename T>
void ConnectionAdapter<T>::disconnect()
{
    bool connected = T::isConnected();
    T::disconnect();
    std::cout << "Log: " << (connected ? "Disconnected" :
        "Doesn't disconnected because connection hasn't been established") << std::endl;
}

Теперь объекты класса ConnectionAdapter можно использовать точно так же, как и объекты типа его параметра.

void tcpFunction(TcpConnection & connection)
{
    connection.connect("example.com", 80);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void sshFunction(SshConnection & connection)
{
    connection.connect("example.com", 22);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void commonFunction(Connection & connection)
{
    connection.connect("example.com", 123);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}


int main()
{
    ::srand(::time(NULL));

    ConnectionAdapter<TcpConnection> tcp_connection;
    ConnectionAdapter<SshConnection> ssh_connection;
    tcpFunction(tcp_connection);
    std::cout << std::endl;
    sshFunction(ssh_connection);
    std::cout << std::endl;
    commonFunction(tcp_connection);
    std::cout << std::endl;
    commonFunction(ssh_connection);
    std::cin.get();
    return 0;
}

Вы не сможете сделать нечто подобное на C#. C# не позволяет наследоваться от параметра обобщения. Таким образом, единственное, что Вам оставляет этот язык -- унаследовать базовый класс, Connection, реализовать все его абстрактные методы при помощи агрегированного объекта конкретного класса, и дописать в двух нужных методах по паре строк кода. И даже сделав это, Вы не сможете использовать объекты данного класса как объекты конкретного класса. Кроме приведённого примера, есть ещё одна очень удобная возможность использовать подобный механизм в купе с приватным наследованием. Передавая в шаблон некоторый тип Вы изменяете реализацию для шаблонного класса, хотя, как уже было сказано, можно заменить это простой агрегацией. Другим серьёзным недостатком обобщений C# является невозможность специализации. Если Вы точно знаете, что для определённого типа данных Ваш шаблон будет работать крайне не оптимально, то Вы можете создать для этого типа отдельную реализацию. C++ позволяет специализировать как все, так и любое количество аргументов шаблона.

#include <iostream>
#include <string>

template<typename T1, typename T2>
class Tuple
{
public:
    Tuple(const T1 & p1, const T2 & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T1 m_p1;
    T2 m_p2;
};

template<typename T>
class Tuple<T, int>
{
public:
    Tuple(const T & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "T, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T m_p1;
    int m_p2;
};

template<>
class Tuple<std::string, int>
{
public:
    Tuple(const std::string & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "std::string, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    std::string m_p1;
    int m_p2;
};


int main()
{
    Tuple<std::string, std::string>("Hello", "World").print();
    Tuple<int, int>(100, 42).print();
    Tuple<std::string, int>("Hello", 42).print();
    std::cin.get();
    return 0;
}

Этот код даст на выходе такой результат

Hello, World
T, int specialization: 100, 42
std::string, int specialization: Hello, 42

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

Препроцессор

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

class Exception
{
public:
    Exception() :
        m_id(0)
    {
    }
    
    explicit Exception(int id) :
        m_id(id)
    {
    }

    Exception(int id, const std::string & message) :
        m_id(id),
        m_message(message)
    {
    }

private:
    int m_id;
    std::string m_message;
};

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

#define DECLARE_EXCEPTION(name)                     \
    class name : public Exception                   \
    {                                               \
    public:                                         \
        name() { }                                  \
        name(int id) :                              \
            Exception(id)                           \
        {                                           \
        }                                           \
        name(int id, const std::string & message) : \
            Exception(id, message)                  \
        {                                           \
        }                                           \
    };

Всё, что нужно написать для определения двух классов -- две строки.

DECLARE_EXCEPTION(NotFoundException)
DECLARE_EXCEPTION(InvalidArgumentException)

Современные IDE, включая Visual Studio, способны выдавать подсказки по сгенерированному таким образом коду.

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

C# ограничивает понятие константности только лишь статическими ссылками на примитивные типы (POD),  дополнительно вводит ключевое слово readonly для того, чтобы ссылке нельзя было переприсвоить объект. Этих механизмов не достаточно, чтобы обеспечить безопасную манипуляцию объектами и избавиться от side-эффектов. C++ предлагает механизм неизменяемых объектов (равно как и переменных POD типов). Если Вы объявляете переменную или параметр функции с модификатором const, то в дальнейшем Вы не можете ни переприсвоить этой переменной значение, ни вызвать из такого объекта метод, который может изменить этот объект. Кроме того, модификатор const может быть применён к типу возвращаемого значения функции. Таким образом, можно реализовать безопасный возврат ссылки или указателя с ограничением только на чтение. Методы, которые не меняют состояние объекта помечаются модификатором const в конце сигнатуры. Внутри константных методов все члены-данные класса неявным образом становятся константами, за исключением тех, что помечены специальным модификатором mutable. Модификатор mutable говорит, что данная переменная не оказывает влияние на состояние объекта и может быть изменена в константных методах. Примером такой переменной может быть счётчик обращений к какому-либо методу. Следующий код вполне валиден  для C#.

class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString() {
        return String.Format("{0}, {1}", LastName, FirstName);
    }
}

class Account {
    readonly Person person;

    public Account(Person person) {
        this.person = person;
    }

    public Person Person {
        get { return person; }
    }
}

class Program {
    static void Main(string[] args) {
        Account account = new Account(new Person {
            FirstName = "John",
            LastName = "Connor"
        });
        account.Person.FirstName = "Isaac";
        account.Person.LastName = "Asimov";
        Console.Out.WriteLine(account.Person);
        Console.ReadKey();
    }
}

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

#include <string>

class Person
{
public:
    Person(const std::string & first_name, const std::string & last_name) :
        m_first_name(first_name),
        m_last_name(last_name)
    {
    }

    void setFirstName(const std::string & first_name)
    {
        m_first_name = first_name;
    }

    const std::string & firstName() const
    {
        return m_first_name;
    }

    void setLastName(const std::string & last_name)
    {
        m_last_name = last_name;
    }

    const std::string & lastName() const
    {
        return m_last_name;
    }

private:
    std::string m_first_name;
    std::string m_last_name;
};

class Account
{
public:
    Account(const Person & person) :
        m_person(person)
    {
    }

    const Person & person() const
    {
        return m_person;
    }

private:
    Person m_person;
};

int main()
{
    Account account(Person("John", "Connor"));
    account.person().setFirstName("Isaac"); // Ошибка
    account.person().setLastName("Asimov"); // Ошибка
    std::cin.get();
    return 0;
}

Строки, помеченные комментарием "Ошибка", к примеру в Visual Studio, приведут к такому сообщению компилятора:

...consoleapplication1.cpp(58): error C2662: 'Person::setFirstName' : cannot convert 'this' pointer from 'const Person' to 'Person &'

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

Ссылки

В C++ существует два способа указывания на объект: классические указатели и ссылки. Указатели являются адресом переменной в памяти, а ссылки -- это механизм, введённый в язык C++ с целью повышения удобства и безопасности. Между ссылками и указателями есть две принципиальных разницы:

  1. ссылке нельзя переприсвоить значение;
  2. ссылка не может быть нулевой.

Первое ограничение касается, скорее, только безопасности, а вот второе очень сильно помогает в работе. В C++ принято принимать в функцию указатель, если параметр может быть равным NULL, в противном случае (чаще всего) принимают  ссылку. В C# ссылки могут быть равными null, что приводит к постоянной проверке всех аргументов и частому возникновению NullReferenceException`ов. Да, в C# параметр можно пометить ключевым словом ref, тогда нельзя будет передать null явно, но следующий код будет вполне валидным.

class Program {
    static void PrintString(ref string str) {
        Console.WriteLine(str);
    }

    static void Main(string[] args) {
        string str = null;
        PrintString(ref str);
        Console.ReadKey();
    }
}

Кроме того, использование ссылок в C++ не ограничено только параметрами, их использование допустимо почти всюду, где допустимо использование переменных.

Копирование объектов

Паттерн проектирования prototype (прототип) предполагает наличие одного объекта (прототипа) и его копирования для создания новых объектов. Такой подход часто обусловлен дороговизной инициализации новых объектов. В языке C++ предусмотрены мощные средства для создания копий объектов -- это конструкторы копирования и операторы присваивания. Конструкторы копирования создают новый объект по переданному образцу, а операторы присваивания заполняют существующий объект из образца. C++ никак не ограничивает программиста в реализации упомянутых методов. Если конструктор копирования или оператор присваивания не определены, то компилятор сгенерирует их автоматически. Копирование по умолчанию подразумевает полное побайтовое копирование объектов.

#include <iostream>
#include <cstring>

class Data
{
public:
    Data(void * data, size_t size);
    Data(const Data & data);
    ~Data();
    Data & operator = (const Data & data);
    inline const void * data() const;

private:
    void * mp_data;
    size_t m_size;
};

Data::Data(void * data, size_t size) :
    m_size(size)
{
    mp_data = ::malloc(size);
    ::memcpy(mp_data, data, size);
    std::cout << "Constructor" << std::endl;
}

Data::Data(const Data & data) :
    m_size(data.m_size)
{
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Copy constructor" << std::endl;
}

Data::~Data()
{
    ::free(mp_data);
    std::cout << "Destructor" << std::endl;
}

Data & Data::operator = (const Data & data)
{
    if(this ==  &data) return *this;
    ::free(mp_data);
    m_size = data.m_size;
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Assignment operator" << std::endl;
    return *this;
}

const void * Data::data() const
{
    return mp_data;
}

void test()
{
    char * content = "Hello, World!";

    Data data1(content, strlen(content) + 1); // Конструктор
    Data data2(data1); // Конструктор копирования
    Data data3(NULL, 0); // Конструктор
    data3 = data2; // Оператор присваивания

    std::cout << data1.data() << ": " << static_cast<const char *>(data1.data()) << std::endl;
    std::cout << data2.data() << ": " << static_cast<const char *>(data2.data()) << std::endl;
    std::cout << data3.data() << ": " << static_cast<const char *>(data3.data()) << std::endl;
}

int main()
{
    test();
    std::cin.get();
    return 0;
}
Constructor
Copy constructor
Constructor
Assignment operator
00489448: Hello, World!
0048A5D0: Hello, World!
0048A620: Hello, World!
Destructor
Destructor
Destructor

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

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

C# при применении оператора копирования копирует лишь ссылку на объект (за исключением значимых типов данных).  Для копирования объектов предназначен интерфейс ICloneable. Этот интерфейс содержит лишь один метод Clone, который возвращает объект типа object. Из этого следует, что клонированный объект всегда нужно приводить к нужному типу (и надеяться на порядочность разработчиков класса).

class Data : ICloneable {
    private byte[] data;

    public Data(byte[] data) {
        this.data = new byte[data.Length];
        data.CopyTo(this.data, 0);
    }

    public byte[] Buffer {
        get { return data; }
    }

    public object Clone() {
        return new Data(data);
    }
}

unsafe class Program {
    public static void Main(string[] args) {
        byte[] content = Encoding.ASCII.GetBytes("Hello, World");
        Data data1 = new Data(content);
        Data data2 = data1.Clone() as Data;
        PrintData(data1);
        PrintData(data2);
        Console.ReadKey();
    }

    static void PrintData(Data data) {
        fixed(byte* ptr = data.Buffer) {
            Console.WriteLine("{0:x}: {1}", (int)ptr, Encoding.ASCII.GetString(data.Buffer));
        }
    }
}
232f454: Hello, World
232f478: Hello, World

Тем не менее, из примера видно, что подход C# справился с поставленной задачей. Но что будет, если необходимо унаследовать один из классов, к исходникам которого у Вас нет доступа и который не реализует ICloneable?

class Data {
    private byte[] data;

    public void Load(string url) {
        using(WebClient client = new WebClient()) {
            data = client.DownloadData(url);
        }
    }

    public byte[] Buffer {
        get { return data; }
    }
}

class DataChild : Data, ICloneable {
    public object Clone() {
        return new DataChild(); // Не полная копия.
    }
}

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

Заключение

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

Асинхронное программирование на C# в Visual Studio 11

На днях я побывал на конференции Windows 8 Camp и увидел немало интересных вещей. Среди них была продемонстрирована новая концепция асинхронного программирования на C#.
Для написания примеров этой статьи я использовал Visual Studio 11 Developer Preview.
Итак, в C#, входящем в состав Visual Studio 11, появились два новых ключевых слова: async и await. Хотелось бы продемонстрировать их работу сразу на примере.
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
const string url = "http://download.mozilla.org/?product=firefox-10.0.2&os=win&lang=en-US";
const string filename = "C:\\firefix.exe";

static void Main(string[] args) {
Test();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test() {
using(WebClient webClient = new WebClient()) {
Task<byte[]> downloadTask = webClient.DownloadDataTaskAsync(url);
byte[] file = await downloadTask;
Console.WriteLine("Downloaded");
using(FileStream stream = File.OpenWrite(filename)) {
Task writeTask = stream.WriteAsync(file, 0, file.Length);
await writeTask;
Console.WriteLine("Written");
}
}
}
}
}
Как видно из примера, кроме новых ключевых слов, в .NET 4.5 появились новые методы. Все методы, поддерживающие асинхронное выполнение, в .NET 4.5 оканчиваются на Async.
Метод, содержащий асинхронное выполнение, должен быть помечен ключевым словом async и делится на две части: до ключевого слова await и после него.
Методы, поддерживающие асинхронное выполнение, должны возвращать значение типа Task или Task<TResult>. Применение операции awiat к объекту Task будет отложено до тех пор, пока задача не будет выполнена. После начала ожидания управление возвращается в рабочий поток, а обработчик будет вызван в другом потоке. Вторая часть метода (после ключевого слова await) и является, по сути, обработчиком события завершения асинхронной операции.
Вывод примера будет следующим:
Как видите, вывод основного потока вклинился между окончанием операции загрузки и завершением сохранения файла.
Нет ничего сложного в написании асинхронных методов самостоятельно. Для этого Вам всего лишь нужно вернуть из Вашего метода значение типа Task или Task<TResult>.  В конструктор такого объекта следует передать делегат, который будет выполняться. Далее, нужно запустить задачу вызовом метода Start и вернуть созданный объект.
using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo {
class Program {
static void Main(string[] args) {
Test2();
while(true) {
Console.Write("-");
Console.Out.Flush();
Thread.Sleep(20);
}
}

static async void Test2() {
Task<string> task = new AsyncWorker().WorkAsync();
string message = await task;
Console.WriteLine(message);
}
}

class AsyncWorker {
public Task<string> WorkAsync() {
Task<string> task = new Task<string>(Worker);
task.Start();
return task;
}

string Worker() {
Console.WriteLine("Step 1");
Thread.Sleep(500);
Console.WriteLine("Step 2");
Thread.Sleep(500);
return "Done";
}
}
}
Класс Task содержит некоторое количество интересных статических методов. Ознакомиться с ними можно в документации msdn.

Отслеживание событий тестирования NUnit

Работая с фреймворком для unit-тестирования .NET сборок NUnit, Вы не можете получить доступ к событиям тестирования из Ваших фикстур (Fixture) напрямую. Для отслеживания событий Вы должны написать add-in. В этой статье я приведу простой пример того, как это можно сделать.
Создадим простую библиотеку классов, которую будем тестировать.
using System;

namespace TestedLibrary {
public class Engine {
uint cylinderCount = 1;
float capacity = 0.0f;
float power = 0.0f;

public uint CylinderCount {
get {
return cylinderCount;
}

set {
if(value < 1) {
throw new Exception("Cylinder count must be great then 0");
}
cylinderCount = value;
}
}

public float Capacity {
get {
return capacity;
}

set {
if(value < 0.0) {
throw new Exception("Engine capacity must be great then 0.0");
}
capacity = value;
}
}

public float Power {
get {
return power;
}

set {
if(value < 0.0) {
throw new Exception("Engine power must be great then 0.0");
}
power = value;
}
}
}
}
namespace TestedLibrary {
public class Car {
public Car(string name, Engine engine) {
Name = name;
Engine = engine;
}

public string Name { get; set; }

public Engine Engine { get; set; }
}
}
Создадим проект для тестирования и добавим в него пару фикстур:
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class EngineTest {
[Test]
public void DefaultValues() {
Engine engine = new Engine();
Assert.AreEqual(0.0f, engine.Power, 0.1f);
Assert.AreEqual(0.0f, engine.Capacity, 0.1f);
Assert.AreEqual(1u, engine.CylinderCount);
}

[Test]
public void ManuallySettedValues() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Assert.AreEqual(135000.0f, engine.Power, 0.1f);
Assert.AreEqual(1995.0f, engine.Capacity, 0.1f);
Assert.AreEqual(4u, engine.CylinderCount);
}
}
}
using NUnit.Framework;
using TestedLibrary;

namespace Testing {
[TestFixture]
public class CarTest {
[Test]
public void Creation() {
Engine engine = new Engine() {
Capacity = 1995.0f,
CylinderCount = 4u,
Power = 135000.0f
};
Car car = new Car("BMW Х3 xDrive20d Urban", engine);
Assert.AreSame(engine, car.Engine);
Assert.AreEqual("BMW Х3 xDrive20d Urban", car.Name);
}
}
}
Теперь можем скомпилировать и запустить unit-тесты для нашей библиотеки
Как я уже говорил, чтобы добраться до событий тестирования, нужно создать add-in для NUnit. Для этого нужно создать новый проект C# из шаблона "Class Library" и добавить в него ссылки на сборки nunit.core.dll и nunit.core.interfaces.dll. По умолчанию они находятся в каталоге C:Program Files (x86)NUnit 2.5.10binnet-2.0lib.
При запуске NUnit просматривает каталог addins, находящийся рядом с исполняемым файлом, и загружает из всех сборок классы с атрибутом NUnit.Core.Extensibility.NUnitAddinAttribute. Кроме того, эти классы должны реализовывать интерфейс NUnit.Core.Extensibility.IAddin, содержащий всего один метод:
namespace NUnit.Core.Extensibility {
public interface IAddin {
bool Install(IExtensionHost host);
}
}
Для того, чтобы подписаться на прослушивание событий, мы должны реализовать интерфейс NUnit.Core.EventListener и передать объект этого интерфейса в метод Install объекта типа NUnit.Core.Extensibility.IExtensionPoint, который нужно получить следующим образом:
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
Наш класс прослушивания будет называться NUnitReporter и его целью будет создание отчёта в формате HTML. В качестве параметра конструктор этого класса будет принемать имя выходного файл. С учётом всего выше сказанного, полный код класса-расширения будет следующим:
using NUnit.Core.Extensibility;

namespace NUnitReport {
[NUnitAddin]
public class Addin : IAddin {
public bool Install(IExtensionHost host) {
IExtensionPoint listeners = host.GetExtensionPoint("EventListeners");
listeners.Install(new NUnitReporter(@"D:report.html"));
return true;
}
}
}
Интерфейс NUnit.Core.EventListener выглядит следующим образом:
namespace NUnit.Core {
public interface EventListener {
void RunFinished(Exception exception);
void RunFinished(TestResult result);
void RunStarted(string name, int testCount);
void SuiteFinished(TestResult result);
void SuiteStarted(TestName testName);
void TestFinished(TestResult result);
void TestOutput(TestOutput testOutput);
void TestStarted(TestName testName);
void UnhandledException(Exception exception);
}
}
Методы RunStarted и RunFinished вызываются при старте и завершении тестирования. В качестве аргумента методу RunFinished передаются результаты всего тестирования или объект возникшего исключения. SuiteStarted и SuiteFinished вызываются для всех сущностей шире единичного теста: класс, пространство имён, Run. Методы TestStarted и TestFinished будут вызваны при запуске и завершении каждого теста. Наконец, метод UnhendledException вызывается, как ясно из названия, при возникновении необработанного исключения.
Приведу простейшую реализацию класса NUnitReporter, собирающего информацию в файл HTML.
using System;
using System.Collections;
using System.IO;
using System.Text;
using NUnit.Core;

namespace NUnitReport {
public class NUnitReporter : EventListener {
string outFileName;
StringBuilder html;

public NUnitReporter(string outFileName) {
this.outFileName = outFileName;
}

public void RunFinished(System.Exception exception) {
}

public void RunFinished(TestResult result) {
PrintRunResult(result);
using(FileStream stream = File.OpenWrite(outFileName)) {
byte[] content = Encoding.UTF8.GetBytes(html.ToString());
stream.Write(content, 0, content.Length);
}
}

void PrintRunResult(TestResult result) {
html = new StringBuilder(
"<html>rn" +
"<head>rn" +
"<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>rn" +
"<title>NUnit Report</title>rn" +
"</head>rn" +
"<body>rn" +
"<div id='run'>rn");
html.AppendLine(String.Format("<h1>{0}</h1>", result.Name));
PrintSuiteResults(result.Results);
html.AppendLine(
"</div>rn" +
"</body>rn" +
"</html>");
}

void PrintSuiteResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='suite'>");
html.AppendLine(String.Format("<h2>{0}</h2>", result.Name));
PrintClassResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintClassResults(IList results) {
foreach(TestResult result in results) {
html.AppendLine("<div class='class'>");
html.AppendLine(String.Format("<h3>{0}</h3>", result.Name));
PrintTestResults(result.Results);
html.AppendLine("</div>");
}
}

void PrintTestResults(IList results) {
foreach(TestResult result in results) {
string stringResult = "";
if(result.IsSuccess) stringResult = "Success";
else if(result.IsFailure) stringResult = "Failure";
else if(result.IsError) stringResult = "Error";
html.AppendLine("<div class='test'>");
html.AppendLine(String.Format("<p>{0}: {1}</p>", result.Name, stringResult));
html.AppendLine("</div>");
}
}

public void RunStarted(string name, int testCount) {
}

public void SuiteFinished(TestResult result) {
}

public void SuiteStarted(TestName testName) {
}

public void TestFinished(TestResult result) {
}

public void TestOutput(TestOutput testOutput) {
}

public void TestStarted(TestName testName) {
}

public void UnhandledException(System.Exception exception) {
}
}
}
Больший интерес во всём примере представляет объект класса NUnit.Core.TestResult. Этот объект содержит информацию о результатах только что завершившегося теста и всех вложенных. К примеру, в результат тестирования пространства имён будут вложены результаты тестирования всех его классов. Эти результаты помещаются в список NUnit.Core.TestResult.Results.
Кроме того, класс NUnit.Core.TestResult имеет множетсво интересных свойств:
public int AssertCount { get; set; }
public string Description { get; }
public bool Executed { get; }
public FailureSite FailureSite { get; }
public virtual string FullName { get; }
public bool HasResults { get; }
public virtual bool IsError { get; }
public virtual bool IsFailure { get; }
public virtual bool IsSuccess { get; }
public string Message { get; }
public virtual string Name { get; }
public IList Results { get; }
public ResultState ResultState { get; }
public virtual string StackTrace { get; set; }
public ITest Test { get; }
public double Time { get; set; }
Файл, содержащий класс Addin следует положить в каталог addins, рядом с исполняемым файлом nunit.exe. Если такого каталога не существует, его нужно создать. Для отладки библиотеки следует аттачиться к процессу nunit-agent.exe (Debug->Attach to Process).
Полученный отчёт будет выгладить так:
Естественно, что приведённый пример сильно упрощён и не годится для реального использования, но, я надеюсь, демонстрирует общий подход.
Исходные тексты проекта можно скачать здесь.

Краткое введение в eXpress Application Framework

Часто тут и там мы слышим о концепции отделения бизнес-логики приложения от UI. Разные софтверные компании предлагают разные решения этой задачи. Microsoft, к примеру, продвигает технологию WPF, а для Qt разрабатывается QML. В этой статье я хочу познакомит Вас с решением от компании Developer Express -- eXpress Application Framework (или просто XAF). Фреймворк предоставляет невероятно много функционала, поэтому я расскажу только о вершине айсберга. Возможно, в следующих статьях, я расскажу больше.
Сразу же стоит отметить, что XAF -- это фреймворк для .NET. Поэтому, если Вы хотите написать кроссплатформенное приложение, то этот фреймворк не для Вас. Под Mono XAF тоже не работает.
Помимо минусов, у XAF есть и положительные качества. К примеру, XAF реализован для WinForms и ASP.NET приложений таким образом, что Вам не нужно задумываться, для какой платформы Вы пишите, конечный продукт будет работать на обеих платформах.
Для того, чтобы начать программировать с использованием XAF не нужно много знать о его устройстве, достатьчно прочесть tutorial на официальном сайте продукта. После установки фреймворка на компьютер, в Visual Studio появляются мастера создания проектов. Вы можете создать проект для WinForms, для ASP.NET или для обеих платформ сразу. Приложения XAF имеют модульную архитектуру.  Мастер сгенерирует несколько проектов, один из которых будет являться общим для всех приложений модулем. Также будут созданы модули для win и web приложений отдельно.
Все эти шаги подробно описаны в указанном tutorial. В этой же статье я приведу пример создания XAF приложения полностью вручную. Скачать демо версию фреймворка Вы можете с официально сайта.

База данных

Для работы приложения XAF Вам понадобится база данных на одной из поддерживаемых СУБД. В приведённом примере я буду использовать MySQL. Придумайте название для Вашего приложения и создайте базу данных с этим именем. Настоятельно рекомендую использовать кодировку UTF-8 для вновь созданной базы MySQL. Мой пример будет носить имя "XafDemo".

Создание базового приложения

Теперь, когда мы обзавелись базой данных, можно приступать к созданию приложения. Для этого создадим в студии каркас Windows Form Application и выкинем из него всё лишнее, оставив только файл Program.cs и ссылку на модуль System. Теперь добавим нужные нам ссылки:
  • System.configuration
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.Images.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1
  • DevExpress.Xpo.v11.1.Providers
  • MySql.Data
Последняя ссылка требует установленного MySQL .NET Connector'а и зависит от той СУБД, которую Вы используете.
Добавим к проекту файл конфигурации и пропишем в нём Connection String до базы данных. Мой конфигурационный файл будет выглядеть так:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ConnectionString" value="XpoProvider=MySql;server=192.168.1.2;user id=sa; password=123456; database=XafDemo;persist security info=true;CharSet=utf8;"/>
</appSettings>
</configuration>

Модуль бизнес-логики

Теперь нужно создать ещё один проект, в котором будет содержаться бизнес-логика нашего приложения. Этот проект должен быть библиотекой классов. Назовём его DemoModule. Так же, как и в первом случае, выкидываем из вновь созданного модуля всё лишнее. Вот те ссылки, которые должны быть в проекте:
  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1
Чтобы XAF заметил наш модуль, мы должны реализовать публичный класс, унаследованный от DevExpress.ExpressApp.ModuleBase. В нашем случае, класс тривиален:
using DevExpress.ExpressApp;

namespace DemoModule
{
public class DemoModule :
ModuleBase
{
}
}

Реализация базового приложения

Теперь у нас есть модуль, который мы можем зарегистрировать в базовом приложении. Возвращаемся в проект XafDemo, добавляем в зависимости проект DemoModule и прописываем его в файле конфигурации
<add key="Modules" value="DemoModule"/>
Для запуска приложения требуется создать экземпляр класса DevExpress.ExpressApp.WinApplication. Обернём создание экземпляра этого класса в класс XafDemoApplication.
using DevExpress.ExpressApp.Win;
using System.Configuration;
using System;
using DevExpress.ExpressApp.Updating;

namespace XafDemo
{
public class XafDemoApplication : IDisposable
{
private WinApplication application;

public XafDemoApplication(string[] arguments)
{
Init();
}

private void Init()
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}

private string LoadConnectionString()
{
return ConfigurationManager.AppSettings["ConnectionString"];
}

private string[] LoadModules()
{
string modules = ConfigurationManager.AppSettings["Modules"];
if(String.IsNullOrEmpty(modules))
return new string[0];
return modules.Split(';');
}

public void Start()
{
application.Start();
}

public void HandleException(Exception exception)
{
application.HandleException(exception);
}

public void Dispose()
{
application.Dispose();
}
}
}
Метод Setup класса WinApplication, в этом примере, принимает три аргумента: имя приложения (соответствует имени базы данных), connection string к базе данных и загружаемые модули. Метод HandleException показывает удобный Message Box с подробным сообщением об ошибке.
Чтобы запустить приложение, достаточно простой реализации метода Main.
using System;

namespace XafDemo
{
public class XafDemo
{
[STAThread]
public static void Main(string[] args)
{
using(XafDemoApplication application = new XafDemoApplication(args))
{
try
{
application.Start();
}
catch (Exception err)
{
application.HandleException(err);
}
}
}
}
}

Однако, при попытке запустить приложение, окажется, что структура базы данных не созана и приложение завершиться с ошибкой. Для того, чтобы база данных была в актуальном состоянии, её следует постоянно обновлять. Добавим метод для обновления базы в класс XafDemoApplication.
public void UpdateDatabase()
{
DatabaseUpdater updater = application.CreateDatabaseUpdater();
updater.Update();
}
Выполнив этот метод, мы поместим всю служебную информацию в базу. Кроме того, будут созданы таблицы для хранения всех бизнес-объектов, о которых чуть позже.

Database updater

Было бы неосмотрительно давать каждому пользователю возможность обновлять базу, поэтому вынесем функционал для обновления в отдельный проект. Для этого создадим Console Application и, по традиции, выкинем всё лишнее из сгенерированного проекта. Назовём новый проект DBUpdater. Понадобятся нам лишь те библиотеки, которые мы использовали в модуле и сам модуль:
  • System
  • DevExpress.Data.v11.1
  • DevExpress.ExpressApp.v11.1
  • DevExpress.ExpressApp.Win.v11.1
  • DevExpress.Xpo.v11.1 
  • DemoModule
Скопируем файл конфигурации из XafDemo в проект и реализуем метод Main.
using System;
using XafDemo;

namespace DBUpdater
{
public class DBUpdater
{
static void Main(string[] args)
{
using(XafDemoApplication application =
new XafDemoApplication(args))
{
try
{
Console.Write("Updating: ");
application.UpdateDatabase();
Console.WriteLine("success!");
}
catch(Exception err)
{
Console.WriteLine("fail!");
Console.WriteLine("Error details:");
Console.WriteLine("Type: {0}", err.GetType().FullName);
Console.WriteLine("Message: {0}", err.Message);
Console.WriteLine("Stack trace:");
Console.WriteLine(err.StackTrace);
Console.WriteLine();
}
Console.Write("Press any key...");
Console.ReadKey();
}
}
}
}
Запустив это приложение, мы создадим рабочую структуру базы. Но есть одна, не очень приятная, особенность. При выполнении метода Setup XAF показывает splash окно. А в консольном приложении оно выглядит глупо. Добавим в XafDemoApplication функционал для отключения splash. Сделать это можно установив null в свойство SplashScreen класса WinApplication. Добавим конструктор, принимающий флаг того, нужно ли отключать окно приветствия, а в методе Init выполним проверку этого флага и установим null в SplashScreen.
public XafDemoApplication(string[] arguments)
{
Init(false);
}

public XafDemoApplication(string[] arguments, bool suppressSplash)
{
Init(suppressSplash);
}

private void Init(bool suppressSplash)
{
application = new WinApplication()
{
Title = "XAF Demo Application"
};
if(suppressSplash)
application.SplashScreen = null;
application.Setup("XafDemo", LoadConnectionString(), LoadModules());
}
Изменим создание XafDemoApplication в DBUpdater, передав true вторым аргументом и противное окно больше не появится.

Первый запуск

Всё готово к первому запуску. Так как мы не реализовали ни одного бизнес-объекта и не прилинковали ни одного стандартного модуля, то окно приложения будет абсолютно пустым

Бизнес-объекты

Чтобы в нашем окне появилось что-нибудь интересное, нужно это создать. Все бизнес объекты, с которыми работает XAF являются объектами ORM eXpress Persistent Objects (XPO), разрабатываемой той же Developer Express. В качестве базового класса для всех бизнес-объектов удобно использовать класс DevExpress.Xpo.XPObject.
Создадим класс Employee, описывающий сотрудника некой фирмы.
using System;
using DevExpress.Xpo;

namespace DemoModule
{
public class Employee : XPObject
{
private string firstName;
private string secondName;

public Employee(Session session) :
base(session)
{
}

public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}
}
}
Как видно, нет ничего сложного. Все открытые свойства, по умолчанию, сохраняются в базе и отображаются на форме. Конструктор класса XPObject принимает объект Session, который создаётся при установлении соединения с базой данных. Чтобы поместить новое значение свойства в базу, следует использовать метод SetPropertyValue, объявленный в классе DevExpress.Xpo.PersistentBase.
Чтобы обновить схему в базе данных, запустите DBUpdater.
После запуска XafDemoApplication мы не увидим ни каких изменений. Это случилось потому, что мы не указали XAF, что хотим видеть.

Модель XAF

Для того, чтобы указать XAF, что мы хотим видеть на форме, мы должны создать файл настроек модели. Добавьте простой текстовый файл с именем Model.DesignedDiffs.xafml в проект DemoModule и установите его свойство "Build Action" в "Embedded Resource". Запишите в этот файл следующий текст
<?xml version="1.0" encoding="utf-8"?>
<Application/>
После пересборки проекта этот файл можно использовать для настройки UI. Делается это либо через встроенный в Visual Studio редактор, либо вручную, запустив программу DevExpress.ExpressApp.ModelEditor.v11.1.exe, которая лежит, у меня, по адресу c:Program FilesDevExpress 2011.1eXpressApp FrameworkToolsModel Editor. В качестве опций эта программа принимает путь до модуля (файла *.dll) и путь до каталога с файлом Model.DesignedDiffs.xafml.
Итак, запустив редактор, мы видим следующее окно

Переходим в раздел NavigationItems и добавляем в пункт Items новый NavigationItem, щёлкнув ПКМ. Это будет наш корневой раздел, назовём его "Organization" (впишите имя в поле Id). Добавим во вновь созданный NavigationItem новый NavigationItem и выберем из списка "View" пункт "Employee_ListView". В полях "Id" и "Caption" выставим имя "Employees". В поле ImageName можно выбрать картинку, которая будет отображаться в пункте навигации. Сохраняем настройки и выходим. После прекомпиляции, в нашем окне добавится новая информация.

Нажав на кнопку "new" мы можем создать новый объект класса Employee.
Можно заметить "лишнее" поле "Oid", которое XPO использует для идентификации объектов в базе данных. Естественным желанием будет удалить это поле. Для этого открываем редактор модели и переходим в пункт Views/DemoModule/Employee_DetailView/Layout/Main/SimpleEditors и удаляем пункт XPObject.

Наложение ограничений на свойства

Наша программа позволяет создать объект класса Employee без имени и фамилии. Таких людей в базе мы видеть не хотим. XAF совместно с XPO позволяют наложить ограничения на поля бизнес-классов простым добавлением атрибутов. Чтобы использовать систему валидации нужно добавить две зависимости: DevExpress.Persistent.Base.v11.1 и DevExpress.ExpressApp.Validation.v11.1. В последнем модуле находится XAF модуль DevExpress.ExpressApp.Validation.ValidationModule. Мы должны добавить зависимость от него в наш модуль DemoModule. Для этого в конструктор DemoModule пишем строку
RequiredModuleTypes.Add(typeof(ValidationModule));
Теперь мы можем наложить атрибут DevExpress.Persistent.Validation.RuleRequiredFieldAttribute на поля FirstName и SecondName. Полученный код будет выглядеть так:
[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.FirstName",
DefaultContexts.Save,
"First Name cannot be empty")]
public string FirstName
{
get { return firstName; }
set { SetPropertyValue<string>("FirstName", ref firstName, value); }
}

[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.SecondName",
DefaultContexts.Save,
"Second Name cannot be empty")]
public string SecondName
{
get { return secondName; }
set { SetPropertyValue<string>("SecondName", ref secondName, value); }
}
После перекомпиляции проекта и обновления базы при попытке сохранить объект без указания имени и фамилии сотрудника мы получим соответствующую ошибку.
Первым параметром атрибута мы указываем уникальный идентификатор валидатора, вторым аргументом указываем, когда нужно запускать проверку. В последнем параметре указывается сообщение, отображаемое в сообщении об ошибке.
Кроме автоматической валидации, модуль DevExpress.ExpressApp.Validation создаёт кнопку "Validate" в тулбаре окна (зелёная галочка), нажав на которую мы можем принудительно провести валидацию.

Отношение ассоциации "один ко многим"

Очень часто возникает ситуация, когда один объект хранит коллекцию других объектов. Фреймворк XPO позволяет создать такое отношение в базе данных, а XAF обеспечит полноценное отображение и удобную работу с такими ассоциациями.
Давайте создадим класс Position и присвоим каждому сотруднику ссылку на объект этого класса. Каждый сотрудник может занимать только одну должность, в то время, как должность содержит список всех сотрудников, которые её занимают.
using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Position :
XPObject
{
private string name;

public Position(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Position.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Position.Name",
DefaultContexts.Save,
"Name cannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Position")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}
Обратите внимание, для гарантирования уникальности имени продукта, я применил атрибут RuleUniqueValue.
Для хранения коллекции объектов в базе данных используется тип DevExpress.Xpo.XPCollection. Чтобы реализовать ассоциацию "один ко многим" нужно объявить свойство типа XPCollection, имеющее только getter. Получить коллекцию ассоциированных объектов можно методом GetCollection. Чтобы XPO и XAF знали что с чем ассоциировано нужно добавить свойству атрибут Association с уникальным именем в параметре. Тот же атрибут нужно указать на другом конце ассоциации. Давайте создадим свойство Position в классе Employee.
private Position position;

[Association("Employee-Position")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Employee.Position",
DefaultContexts.Save,
"Position cannot be empty")]
public Position Position
{
get { return position; }
set { SetPropertyValue<Position>("Position", ref position, value); }
}
С этой стороны ассоциации свойство выглядит так, как и все остальные, за исключением атрибута Association. Откомпилировав приложение, обновив базу, отредактировав модель и запустив программу мы можем создать несколько должностей и сотрудников и сассоциировать их. Примерный результат работы можно видеть на следующем изображении.

Отношение ассоциации "многие ко многим"

Не всегда достаточно отношения "один ко многим". Часто бывает необходимо, чтобы один объект хранил коллекцию других объектов, а другие объекты хранили коллекции первых объектов. XPO и XAF поддерживают и эту концепцию. Для её демонстрации, предположим, что наша организация занимается производством нескольких видов продуктов. Каждый сотрудник может принимать участие в производстве нескольких продуктов, равно как и один продукт производится несколькими сотрудниками. Создадим простой класс Product.
using System;
using DevExpress.Xpo;
using DevExpress.Persistent.Validation;

namespace DemoModule
{
public class Product :
XPObject
{
private string name;

public Product(Session session) :
base(session)
{
}

[RuleUniqueValue(
"RuleUniqueValue for DemoModule.Product.Name",
DefaultContexts.Save,
"Name must be unique")]
[RuleRequiredField(
"RuleRequiredField for DemoModule.Product.Name",
DefaultContexts.Save,
"Namecannot be empty")]
public string Name
{
get { return name; }
set { SetPropertyValue<string>("Name", ref name, value); }
}

[Association("Employee-Product")]
public XPCollection<Employee> Employees
{
get { return GetCollection<Employee>("Employees"); }
}
}
}
Для описания ассоциации "многие ко многим" используется тот же принцип, что и при использовании ассоциации "один ко многим", только на обоих концах ассоциации находятся свойства, возвращающие коллекции.
[Association("Employee-Product")]
public XPCollection<Product> Products
{
get { return GetCollection<Product>("Products"); }
}
По традиции, компилируем проект, обновляем базу данных, редактируем модель, запускаем. После некоторых манипуляций с данными мы можем получить такой результат.

Пользовательские элементы управления

XAF поддерживает создание пользовательских элементов управления. Для этого нужно реализовать контроллер для одного или нескольких отображений и в нём создать объект одного из наследников класса DevExpress.ExpressApp.Actions.ActionBase. Приведу пример лишь самого простого из них -- SimpleAction, который представляет собой кнопку на тулбаре. Наш action будет отображаться в детальном просмотре объектов класса Employee и, при активации, очищать список продуктов сотрудника.

using System;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.Persistent.Base;

namespace DemoModule
{
public class EmployeeController :
ViewController<DetailView>
{
private SimpleAction cleanProductsAction;

public EmployeeController()
{
TargetObjectType = typeof(Employee);
cleanProductsAction = new SimpleAction(this,
"CleanProductsFromEmployee", PredefinedCategory.RecordEdit)
{
Caption = "Clean all products"
};
}

protected override void OnActivated()
{
base.OnActivated();
cleanProductsAction.Execute += OnCleanProductsActionExecute;
}

protected override void OnDeactivated()
{
base.OnDeactivated();
cleanProductsAction.Execute -= OnCleanProductsActionExecute;
}

private void OnCleanProductsActionExecute(object sender,
SimpleActionExecuteEventArgs e)
{
Employee employee = View.CurrentObject as Employee;
if(employee == null)
return;
int count = employee.Products.Count;
for(int i = count - 1; i >= 0; --i)
{
employee.Products.Remove(employee.Products[i]);
}
if(count > 0)
View.ObjectSpace.SetModified(employee);
}
}
}
Для создания контроллера требуется указать тип объекта, на представлении которого он будет активироваться. Первым параметром конструктора класса SimpleAction передаём ссылку на родительский контроллер. Затем передаётся идентификатор action'а и раздел, куда этот action будет помещён.

При активации контроллера вызывается метод OnActivated, в котором мы подписываемся на событие "Execute".
Контроллер содержит в себе ссылку на вид, на котором был активирован в свойстве View. Через этот объект мы можем получить текущий объект, обратившись к свойству CurrentObject. На всякий случай проверим его тип и, перебрав все продукты, удалим их. Изменения в списке XAF не заметит и кнопка "Save" не будет активирована. Для устранения этого эффекта вызываем метод SetModified из пространства объектов текущего вида.

Заключение

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

C#, кто он?

Не так давно я начал осваивать нынче сверхпопулярный, среди windows программистов, язык программирования C#. Осваивать я его начал, естественно, не просто так, а с корыстными целями, но об этом я рассказывать не буду. Нельзя сказать, что до этого я его в глаза не видел, писал я маленький тестик. Теперь же, после недельного знакомства с этим языком, я хочу поделиться своими впечатлениями.Сразу хочу предупредить, что всё, написанное в этой статье, является моим субъективным мнением.

Платформа .NET

Для работы программ, созданных на платформе .NET, то есть всех программ на языке C#, требуется виртуальная машина .NET. Платформа .NET должна была создать конкуренцию платформе Java, но разработчики Microsoft остановились на поддержке только операционных систем семейства Windows, похоронив тем самым кроссплатформенность. Однако компания Novell разрабатывает альтернативную кроссплатформенную реализацию .NET и C#. Этот проект получил название Mono.
Даже несмотря на то, что Microsoft предоставила Novell все права, проект Mono остаётся догоняющим. По этой причине проектирование Mono сильно отстаёт. Например, WPF в проекте Mono даже и не собираются реализовывать.Исходя из вышесказанного, нетрудно предположить, что мы можем либо писать только под Windows, либо принять ограничения Mono и жить в их рамках.
Но не всё так плохо, того, что реализовано проектом Mono достаточно для создания полноценных приложений. Открытым остаётся вопрос, а согласятся ли компании, создающие ПО на C#, на такие ограничения?
Проект .NET создавался сразу с несколькими целями, давайте разберёмся, что это за цели и каков результат.

Замена устаревшим технологиям COM и ActiveX

Позволю себе напомнить о том, что такое COM и ActiveX. Технология COM служит для межпроцессного, внутрепроцессного и сетевого взаимодействия объектно-ориентированных модулей. Взаимодействие строится на понятиях виртуальной таблицы (VTBL) и интерфейса класса. Приложение, которое хочет воспользоваться объектом другого модуля, просит этот модуль создать ему экземпляр какого-либо класса и, получив его, инициализирует специальным образом, заранее известный, интерфейс. Системой маршалинга COM вызовы виртуальных методов интерфейса преобразуются в корректные вызовы и все остаются довольны. Технология ActiveX - это надстройка над COM, которая служит для работы с графическим интерфейсом, обеспечивая такие понятия как компонент, узел компонента и обмен сообщениями.
Итак, как же технология .NET решает те же проблемы, что и COM. Во-первых .NET вводит понятие сборки (assembly), которая является её функциональной частью. Сборка представляет из себя бинарный модуль, хранящий полную информацию о том, что он содержит: пространства имён, зависимости, типы, методы итд. Любая сущность, объявленная в сборке как public автоматически становится доступной из других сборок. При том все события и делегаты остаются в рабочем состоянии, а виртуальная машина .NET обеспечивает, как и в случае с COM, все межпроцессные, внутрипроцессные и сетевые взаимодействия. Таким образом, отпадает необходимость в виртуальных таблицах, а значит и работа с графическими компонентами становится неотличимой от простого взаимодействия.
С первой задачей .NET справился, но на этом создатели не остановились. Раз уж нам известно всё о типах, находящихся внутри сборки, то пользователь этой сборки может расширять все типы, унаследовав их. Технология COM не позволяет наследоваться от классов из скомпилированного модуля.

Мультиязычная поддержка

В рамках платформы .NET реализована технология под названием CLR (Common Language Runtime). Это технология, позволяющая работать с платформой .NET из других языков программирования. Также, предоставляется возможность взаимодействия сборок, написанных на разных языках.
В рамках технологии CLR реализовано множество языков, как самой корпорацией Microsoft, так и другими компаниями. Среди этих языков, такие языки как C#, Visual Basic.NET, C++.NET, F#, IronPython, Delphi, Nemerle и многие другие. Так как я не работал со многими из тех языков, что значатся в списке, поддерживающих CLR, я скажу только о C++.NET. Достаточно справедливо будет отметить, что разработчики компании Microsoft внесли такие изменения в язык C++, что работать с ним стало просто не приятно. Попробовав поработать с C++.NET, я понял, что не выдержу издевательств над своим любимым языком и поспешно удалил тестовый проект. Больше я к нему не возвращался. Получается, что не CLR поддерживает язык, а язык изменили для CLR (то есть подвинули гору к Магомету). Из этого можно сделать вывод, что некоторые языки, как минимум C++ и Visual Basic подгонялись под платформу .NET только для того, чтобы создать изначальный пул языков CLR. Надеюсь, что с другими языками, которые создавались не в такой спешке, подобной истории не случилось.
Подводя итоги для этой цели, можно сказать, что платформа .NET не полностью справилась с поставленной задачей, так как я вынужден использовать не C++, а его модификацию только потому, что разработчикам .NET так захотелось. Но нельзя отрицать тот факт, что поддерживаемых языков, действительно, много, и их количество возрастает.

Язык C#

Давайте же перейдём к виновнику сей статьи - к языку C#. Язык C# - это первый из языков CLR, который создавался с нуля и специально для платформы .NET, поэтому он должен отражать все намерения разработчиков. Язык разрабатывался под сильным влиянием языка Java и должен был стать его конкурентом. В этой статье я не буду писать учебник по этому языку, о нём написано много книг и на msdn есть руководство. Здесь я расскажу о тех конструкциях языка, которые мне понравились и о тех, которые мне не понравились.

Наследование

Как и в Java, в C# запрещено множественное наследование классов, но разрешено множественное наследование интерфейсов. Хорошо это или плохо? Споры по этому поводу не умолкают с тех пор, как придумали наследование. Моё мнение - плохо. Ведь у каждого программиста есть своя собственная голова на плечах и он ею может подумать и сам решить все проблемы, которые несёт неверное использование множественного наследования, а современные компиляторы способны подсказать ему, что в коде возникла путаница. А вот пользы от множественного наследования гораздо больше, чем вреда. Если вспомнить истоки ООП, то основной причиной создания этого подхода было определение типов, соответствующих реальным объектам и предметам. А коли это так, то "диван-кровать" и "компьютер моноблок" являются примерами таких реальных объектов, которые в ООП было бы правильно описывать множественным наследованием (от "дивана" и "кровати" в первом случае и от "системный блок" и "монитор" - во втором).

Упаковка объектов

В C# все типы неявно наследуются от типа System.Object или просто object. Тип object используется для, так называемой, упаковки объектов. Упаковка в синтаксическом виде - это ни что иное, как приведение объекта к типу object
string str = "Hello";
object obj = (object)str;
Надо отметить, что не смотря на все советы Скотта Мэйерса, приведение типов в C# осталось в стиле языка Си.
Итак, что же особенного в данной ситуации. Упаковка объектов в платформе .NET используется повсеместно, что может привести к путанице. Конечно, при неверном приведении среда исполнения выдаст исключение, но мне представляется, что runtime - это уже поздно. Ловить исключения при каждом присвоении смешно, согласитесь.
В качестве примера использования упаковки можно привести операцию клонирования. В интерфейсе IClonable объявлен метод Clone, который возвращает тип object. Служит этот метод только одной цели - создание копии объекта. Давайте посмотрим, на то, как выглядит определение и использование клонирования.
class Sheep : ICloneable
{
private string theName;
private int theAge;

public Sheep(string aName, int anAge)
{
theName = aName;
theAge = anAge;
}

public object Clone()
{
return new Sheep(theName, theAge);
}
}

class Program
{
static void Main(string[] args)
{
Sheep dolly = new Sheep("Dolly", 1);
Sheep dolly2 = (Sheep)dolly.Clone();
}
}
Представляется мне это всё, мягко говоря, не удобным и плохо читабельным.

Копирование объектов

Раз уж я заговорил о клонировании, то добью эту тему до конца. В C# все типы данных поделены на два вида: структурные и ссылочные. Структурные - это те типы, которые мы, C++ программисты, привыкли называть POD (Primitive Old Data) типами (int, float, bool, ...), перчисления и структуры - которые в C# обрели новую жизнь. Все структурные типы являются наследниками типа System.ValueType и память для их объектов всегда выделяется в стеке. Все остальные типы, то бишь классы и интерфейсы, называются ссылочными и наследуются, как уже было сказано, от System.Object или любого производного, кроме System.ValueType. Память для объектов ссылочных типов выделяется в управляемой куче.
До сих пор всё выглядит сносно, но теперь давайте представим всё на деле. Допустим, у нас есть некая структура, содержащая какие-то данные.
struct Data
{
public int theData;

public Data(int aData)
{
theData = aData;
}
}

class Program
{
static void PrintIncrementedData(Data data)
{
Console.Write("Incremental data: {0}n", ++data.theData);
}

static void Main(string[] args)
{
Data data = new Data(50);
PrintIncrementedData(data);
PrintIncrementedData(data);
}
}
Данная программа не выглядит реально, но тем не менее, ведёт себя так, как мне хочется, а именно, выводит такой результат
Incremental data: 51
Incremental data: 51
Теперь давайте представим, что тот, кто сопровождает эту структуру неожиданно переименует её в класс, ну, допустим, ему понадобится конструктор по умолчанию, который запрещено создавать в структурах C#. Тогда вывод нашей программы внезапно приобретает другой вид
Incremental data: 51
Incremental data: 52
Одно слово и наша программа перестала работать так, как положено, но продолжила компилироваться без ошибок и предупреждений. Всё это произошло по одной причине: структурные типы при присваивании и передачи в качестве параметров копируются по значению, а ссылочные, как видно из названия, - копируют только ссылку на объект в памяти.
Не знаю, какой тайный смысл разработчики C# вложили в структуры, но трудно обнаруживаемые ошибки, связанные с их использованием представляются более, чем вероятными.

Константность

Здесь я хотел бы поговорить о таких понятиях, которые с C++ называются константными методами и константными возвращаемыми значениями. Если быть точнее, то возвращаемое значение просто имеет константный тип. Но формулировки не столь важны, в C# нет ни того, ни другого. Методы в C# всегда возвращают ссылку на объет ссылочного типа или копию объекта структурного типа. Чем это чревато. Давайте посмотрим на пример из книги "C# и платформа .NET" Эндрю Троелсена, описанный в третьей главе, в разделе, посвящённом инкапсуляции. Так как этот пример там размазан по нескольким параграфам, я соберу его и немного дополню, смысл от этого не изменится.
class FullName
{
public string firstName;
public string secondName;

public FullName(string aFirstName, string aSecondName)
{
firstName = aFirstName;
secondName = aSecondName;
}
}

class Emploee
{
private FullName theFullName;

public Emploee(FullName aFullName)
{
theFullName = aFullName;
}

public FullName Name
{
get { return theFullName; }
}
}


class Program
{
static void Main(string[] args)
{
Emploee emploee = new Emploee(new FullName("William", "Gates"));
FullName name = emploee.Name;
name.firstName = "Linus";
name.secondName = "Torvalds";
Console.Write("1: {0} {1}n", emploee.Name.firstName,
emploee.Name.secondName);
Console.Write("2: {0} {1}n", name.firstName, name.secondName);
}
}
Не смотря на то, что автор утверждает, что свойство Name доступно только для чтения, я не применяя никаких усилий изменил значение приватной переменной, в чём легко убедиться, скомпилировав и запустив программу.
1: Linus Torvalds
2: Linus Torvalds
Стоит ли упоминать, что подобное поведение может привести к, довольно плачевным, последствиям. Для того, чтобы избежать такой ситуации не остаётся ничего другого, кроме как скопировать объект FullName перед возвратом из свойства.
public FullName Name
{
get { return new FullName(theFullName.firstName,
theFullName.secondName); }
}
Естественно, такой подход гораздо более затратный, нежели возврат константной ссылки в C++.

Делегаты

Увидев в первый раз концепцию делегатов, я пришёл в восторг. Это, действительно, мощная и простая технология позволяет создавать такие конструкции, для которых в C++ применялись колбеки, функторы и обсерверы. Если вкратце, то делегат - это особый вид функции, который объявляется в одном классе, а реализован может быть в другом. Кроме того, один делегат может быть реализован много раз и, при вызове, такого делегата, будут вызваны все его реализации. Давайте посмотрим на примере.
class People
{
public delegate void SpeakDelegate();
public SpeakDelegate Speak;
}

class Man
{
private string theName;

public Man(string aName)
{
theName = aName;
}

public void Speak()
{
Console.Write("Hello, my name is {0}n", theName);
}
}

class Program
{
static void Main(string[] args)
{
Man jhon = new Man("Jhon");
Man alice = new Man("Alice");
People people = new People();
people.Speak +=
new People.SpeakDelegate(jhon.Speak) +
new People.SpeakDelegate(alice.Speak);
people.Speak();
}
}
При вызове people.Speak будут вызваны методы Speak двух объектов - jhon и alice, а результат будет таким
Hello, my name is Jhon
Hello, my name is Alice
Этот же механизм (с небольшими дополнениями) используется при отправке сообщений. Но об этом я говорить не буду.
В общем-то, всё это выглядит очень красиво, но стоит помнить одну деталь - вызов делегата, у которого нет подписчиков приведёт к генерации исключения и крешу программы, если его не обработать. Следовательно, при вызове делегата мы должны каждый раз проверять, инициализирован ли он, благо не инициализированные делегаты всегда равны null. На первый взгляд, может показаться, что разработчики могли бы гарантировать безопасный вызов делегатов. Но нет, делегаты могут возвращать значение, а какое значение может вернуть то, что не было вызвано? Раз уж я об этом заговорил, то возвращаемым значением делегата - это значение, которое вернёт последняя вызванная делегатом функция.

Оператор foreach

Конечно же, каждый программист на C++ мечтает увидеть в своём любимом языке оператор обхода коллекций foreach. В языке C# он существует. При том синтаксис этого оператора весьма удобен, а работать с ним одно удовольствие.
foreach(ТипЭлементаКоллекции ссылка in коллекция)
{
}
"ссылка" принимает значение каждого из элементов и является именно ссылкой, а не копией. Чтобы Ваш класс мог работать как коллекция в операторе foreach, он должен реализовать интерфейс IEnumerable, в котором всего один метод - GetEnumerator. Вот если Вы используете не встроенные коллекции, то Вам придётся реализовать и интерфейс IEnumerator.

Коллекции

Список всевозможных коллекций в .NET достаточно большой, но что более всего меня вводит в недоумение - это то, что большинство из них не являются обобщёнными. Например, класс ArrayList хранит объекты типа object. Как я уже говорил в разделе об упаковке объектов, это может вызвать путаницу. По какой причине часть контейнеров сделали обобщёнными, а часть - хранящими объекты типа object для меня остаётся загадкой.

Разные мелочи

Из того, что я видел только в C#, стоит отметить статические конструкторы. Это особый вид конструкторов, в котором можно инициализировать статические члены класса. Кроме того, статические члены можно инициализировать прямо при объявлении, даже вызовом какой-либо функции.
Не удалось мне найти достойной замены оператору typedef из C++. Можно использовать ключевое слово using, но распространятся такой "typedef" будет только внутри одного модуля.

Итоги

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