@Azarien napisał dzisiaj, że Nie ma powodu, by C++/CLI musiało być tylko do „portowania bibliotek” poza tym, że wielu tak uważa
. Chciałbym poruszyć ten wątek, a że w tamtym w gruncie rzeczy mowa o innym języku to założyłem nowy.
Ostatnimi czasy widać w Polsce zwiększające się zainteresowanie językiem C++/CLI. Jak dla mnie, wynika to z niczego innego jak próbą pójścia na skróty w nauce czystego C++. Wiadomo: czytelniejsza biblioteka standardowa, a klepanie interfejsu użytkownika w Visual Studio nigdy nie było prostsze. Koniec końców najczęściej jest taki, że przychodzi człowiek na forum, bo nie może sobie poradzić ze składnią tego tworu i okazuje się, że nie poznał ani C++, ani .NET.
Także apeluję do młodszych (stażem, bo sam młody wiekiem jestem) programistów: nie zabierajcie się za C++/CLI, jeżeli chcecie się nauczyć C++ oraz nie zabierajcie się za C++/CLI, jeżeli chcecie nauczyć się .NET.
W pierwszym przypadku uczcie się po prostu C++. Po dogłębnym przestudiowaniu wcale niedużej biblioteki standardowej ze zdziwieniem stwierdzicie na koniec, że i tak zaoszczędzi wam kupę czasu, bo przykładowo nie trzeba już korzystać z ręcznie klepanych pętli czy mieszać gdzieś w kod bibliotekę C do operowania na napisach, bo nigdy wcześniej nie spodziewaliście się, że nagłówek <algorithm>, iteratory, insertery, i/o stringstreamy mogą być tak proste i przydatne. Można pójść dalej i poznać nowe (w standardzie) inteligentne wskaźniki i na zawsze zrezygnować z ręcznego zarządzania pamięci.
Jeżeli z kolei podoba wam się przeciąganie kontrolek na formę w Visual Studio i nie macie odgórnego wymogu (ze szkoły, uczelni) na naukę C++ to bezwzględnie piszcie od razu w C#. O ile na samiutkim początku może zbyt dużej różnicy nie ma, bo kompilator i tak wygeneruje kod za was, to już za chwilę zababracie się zupełnie w C++/CLI. Nie jest to wcale dziwne, bo nie dość, że C++ jest językiem skomplikowanym to dołączenie do niego zawiłości CLR z niedoskonałą (w porównaniu do C#, gdzie twórcy języka mogli zaprojektować go od zupełnych podstaw) składnią prowadzi do katastrofy. A idzie nam jeszcze Windows 8, WinRT i kolejna odmiana C++, czyli C++/CX z podobnymi, ale znów innymi i prowadzącymi do kolejnych nieporozumień rozszerzeniami.
Spójrzcie z resztą na tą bardzo prostą klasę, którą napisałem jakiś czas temu w C#.
namespace Rev.PhotoAlbum.Application.Modules
{
using Autofac;
using NHibernate;
using NHibernate.Cfg;
using Rev.PhotoAlbum.Model.Configuration;
using Rev.PhotoAlbum.Model.Repositories;
public class DataAccessModule : Module
{
protected override void Load(ContainerBuilder builder)
{
var configuration = Configurator.BuildConfiguration();
var sessionFactory = Configurator.BuildSessionFactory(configuration);
RegisterComponents(builder, configuration, sessionFactory);
}
private void RegisterComponents(ContainerBuilder builder, Configuration configuration, ISessionFactory sessionFactory)
{
builder.RegisterInstance(configuration).As<Configuration>().SingleInstance();
builder.RegisterInstance(sessionFactory).As<ISessionFactory>().SingleInstance();
builder.Register(x => x.Resolve<ISessionFactory>().OpenSession()).As<ISession>().InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(typeof(IRepository<>).Assembly)
.Where(t => t.IsClosedTypeOf(typeof(Repository<,>)))
.AsClosedTypesOf(typeof(IRepository<>));
}
}
}
Nie ma co prawda w kodzie komentarzy, ale dzięki odpowiedniej konstrukcji frameworków, z których korzystam kod ten można.. po prostu przeczytać - ciągiem. Układają się z tego zdania i na pierwszy rzut oka można się domyślić co się dzieje - chodzi mi zwłaszcza o metodę RegisterComponents
. Jeżeli ktoś nigdy z AutoFaca nie korzystał, a ma minimalną wiedzę z teorii na temat kontenerów IoC mógłby własny kod napisać po jedno, dwu-krotnym przeczytaniu mojego.
A teraz ten sam kod w C++/CLI.
Najpierw oczywiście plik nagłówkowy, jako sumienni programiści C++ nie wrzucamy wszystkiego do jednego pliku.
#pragma once
namespace Rev
{
namespace PhotoAlbum
{
namespace Application
{
namespace Modules
{
ref class DataAccessModule : public Autofac::Module
{
private:
void RegisterComponents(Autofac::ContainerBuilder^ builder, NHibernate::Cfg::Configuration^ configuration, NHibernate::ISessionFactory^ sessionFactory);
protected:
virtual void Load(Autofac::ContainerBuilder^ builder) override;
};
}
}
}
}
Ah, no tak. C++ nie obsługuje zagnieżdżonych przestrzeń nazw w jednej instrukcji, musimy rozbić je na kilka. Tutaj pedantycznie trzymam się formatowania, które sobie przyjąłem, możemy zawsze wrzucić wszystkie namespace'y do jednej linijki albo - jak polecają w internecie - stworzyć sobie makro. A jak.
Rozciągnął się kod niesamowicie, no, ale przecież mamy na uwadze zasadę nieużywania using namespace
w plikach nagłówkowych, żeby nie potworzyć żadnych konfliktów nazw w plikach, gdzie będziemy nasz plik nagłówkowy dołączać.
using namespace Rev::PhotoAlbum::Application::Modules;
using namespace Autofac;
using namespace NHibernate;
using namespace NHibernate::Cfg;
using namespace Rev::PhotoAlbum::Model::Configuration;
using namespace Rev::PhotoAlbum::Model::Repositories;
void DataAccessModule::Load(ContainerBuilder^ builder)
{
auto configuration = Configurator::BuildConfiguration();
auto sessionFactory = Configurator::BuildSessionFactory(configuration);
RegisterComponents(builder, configuration, sessionFactory);
}
ISession^ ISessionResolve(IComponentContext^ context)
{
return ResolutionExtensions::Resolve<ISessionFactory^>(context)->OpenSession();
}
bool IRepositoryWhereCondition(System::Type^ type)
{
return TypeExtensions::IsClosedTypeOf(type, Repository::typeid);
}
void DataAccessModule::RegisterComponents(ContainerBuilder^ builder, Configuration^ configuration, ISessionFactory^ sessionFactory)
{
RegistrationExtensions::RegisterInstance(builder, configuration)->As<Configuration^>()->SingleInstance();
RegistrationExtensions::RegisterInstance(builder, sessionFactory)->As<ISessionFactory^>()->SingleInstance();
RegistrationExtensions::Register<ISession^>(builder, gcnew System::Func<IComponentContext^, ISession^>(&ISessionResolve))->As<ISession^>()->InstancePerLifetimeScope();
RegistrationExtensions::AsClosedTypesOf(RegistrationExtensions::Where(RegistrationExtensions::RegisterAssemblyTypes(builder, IRepository::typeid->Assembly),
gcnew System::Func<System::Type^, bool>(&IRepositoryWhereCondition)), IRepository::typeid);
}
Chciałbym zwłaszcza zwrócić na końcówkę metody RegisterComponents
. Konia z rzędem temu, kto rozszyfruje co się tutaj dzieje. Niestety (albo stety) bardzo wiele frameworków, które ułatwiają (taa) pisanie w .NET korzystają z metod rozszerzających (z resztą zdaje się, że bardzo dobrze, zgodnie z zasadą open/closed). Z jakiś powodów najzwyczajniej nie są one składniowo obsługiwane w C++/CLI i trzeba wywoływać je ręcznie, z klasy, w której są zdefiniowane i przekazywać ręcznie obiekt. Bez tracenia dodatkowego czasu na znalezienie chociażby nazwy tej klasy się nie obejdzie. Kolejna rzecz to lambdy, które w C# niesamowicie ułatwiają sprawę, a w C++/CLI powodują, że musimy je wydzielić do zewnętrznej metody. Lambdy z C++11 nawet w Visual Studio 11 nie są z nimi kompatybilne. No trudno.
Ktoś mógłby powiedzieć, że klepanie nawet takiego kodu z IntelliSense nie będzie trudne. Nawet dodali do niego obsługę C++/CLI. Bardziej przykra sprawa jest taka, że na co trzeciej bardziej rozbudowanej linijce z typami generycznymi po prostu się wykłada i zaczyna krzyczeć o błędach. Których de facto nie ma, bo kod się kompiluje i działa.
Wykorzystanie w C#:
var builder = new ContainerBuilder();
builder.RegisterModule<Modules.DataAccessModule>();
var container = builder.Build();
var repository = container.Resolve<IRepository<Person>>();
I C++/CLI:
auto builder = gcnew ContainerBuilder();
RegistrationExtensions::RegisterModule<Modules::DataAccessModule^>(builder);
auto container = builder->Build(Builder::ContainerBuildOptions::Default);
auto repository = ResolutionExtensions::Resolve<IRepository<Person^>^>(container);
Nie domyślił się wartości domyślnej parametru, a IntelliSense rzucił malowniczy błąd w ostatniej linii o tym, że wywołanie pasuje do wielu przeładowań oraz jednocześnie ma za mało parametrów.
Nie mówię, że pisanie w C++/CLI nie ma w ogóle sensu. Ma, ale są to naprawdę marginalne zastosowania, gdy nie wystarcza nam P/Invoke i interop w C#. Ja jeszcze nigdy nie musiałem go naprawdę użyć. Wy pewnie też nie.