Drobná poznámka ke kontravariancí delegátů v C#
Předpokládám, že se stejně jako já těšíte na lepší podporu kovariance a kontravariance u rozhraní a delegátů v připravované verzi C# 4.0. Už dnes se ale dá s existující podporou kovariance a kontravariance u delegátů pěkně kouzlit – pro ty s exaktnějším přístupem ke kódu a vytříbenou terminologií se slovo “kouzlit” v knihách zásadně překládá jako “psát elegantnější kód”. Opakovat základy kovariance a kontravariance u delegátů zde nebudu a všechny ty, kteří sem zabloudili při svém ahashverovském “googlování” nějakého problému volně souvisejícího s probíraným tématem, odkážu na článek v MSDN.
Kovarianci i kontravarianci delegátů používám rád, ale dnes se mi podařilo narazit na potíž, o které si nejsem jistý, že je všeobecně známa. Alespoň já jsem se po chvíli údivu a narůstajícího rozčilení nad tím, že můj dokonalý kód nechce vybíravý kompilátor přijmout a neustále protestuje, musel zbaběle uchýlit ke specifikaci C# 3.0.
Takže zde je popis “problému”.
Tento kód asi nikoho nepřekvapí
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> del = Test;
}
public static void Test(Base p)
{
Console.WriteLine(p);
}
}
Máme generického delegáta MyAction, jehož instanci s názvem del vytvoříme v metodě Main. Za generický parametr T dosadíme generický argument typu “Derived” , přičemž delegát ukazuje na metodu Test, která přijímá argument Typu Base. Kontravariance zajistí, že tento kód bez problémů projde.
Zkusme udělat mírnou úpravu. Místo metody Test přiřadíme do delegáta del lambda výraz, u kterého explicitně určíme typ argumentu.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ContravarianceTest
{
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> a = (Base b) => Console.WriteLine(b) ;
}
}
}
Kompilátor se tentokrát razantně ohradí proti podobné manipulaci.
Cannot convert lambda expression to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs
A v další chybě své výhrady upřesní.
Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived' C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs
Shrneme-li to, je zřejmé, že při použití lambda výrazu kompilátor na nějakou kontravarianci argumentů zapomene a vyžaduje, aby typ argumentu v lambda výrazu byl identický s typem argumentu u delegáta. Ještě podotknu, že stejné chování s projeví i u negenerického delegáta - public delegate void MyAction(Derived d);.
Použijeme-li anonymní metodu, kompilátor stále protestuje jako u lambda výrazů.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ContravarianceTest
{
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> a = delegate(Base b)
{
Console.WriteLine(b);
};
}
}
}
Cannot convert anonymous method to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs 27 35
Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived' C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs 27 44
U anonymních metod a lambda výrazů tedy kontravarianci nehledejme. Ve specifikacci C# 3.0 (C# 3.0 specification – sekce 7.14.1) nalezneme popis omezení pro anonymní metody a lambda výrazy.
“If an anonymous function has an explicit-anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have the same parameter types and modifiers in the same order. In contrast to method group conversions (§6.6), contra-variance of anonymous function parameter types is not supported. [zvýraznil R.S.] If an anonymous function does not have an anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have no out parameters.”
Většina programátorů v C# asi nikdy na podobné omezení nenarazí, a když už ano, vezmou jako fakt, “že to asi z nějakého důvodu nejde”. Mně přijde podobné chování neintutivní a vsadil bych se, že vetšina vývojářů, ať už vědomě či podvědomě, na C# oceňuje, že jde o jazyk, ve kterém na ně nečíhá mnoho záludností nebo nepříjemných překvapení. S přidáváním dalších a dalších rysů do jazyka vzrůstá pravděpodobnost, že některá nová vlastnost začne ovlivňovat způsob použití starší vlastnosti v jazyce a také vzrůstá počet rozdílů mezi konstrukcemi, které na první pohled vypadají stejně, nebo alespoň od nich poučenější vývojář, ačkoli si je vědom některých rozdílů například mezi delegátem ukazujícím na tradiční funkci, anonymní metodu, lamda výraz a složenou lambdu (lambda statement), očekává podobné chování.
Celou poznámku bychom mohli uzavřít dotazem: “Kolik nových vlastností programovací jazyk snese bez šrámů a posléze pořádných zářezů na pověsti “jednoduchého” jazyka? Tipnul bych si, že empiricky si to budeme moci ověřit, až se začnou na fórech množit zoufalí vývojáři naříkající nad složitostí jazyka, jako se to děje dnes, když nějaká lama poté, co zbastlila při svých hrátkách “skorofunkčnípůlaplikaci” ve VB, Javě či C#, je nucena programovat v C++”? Jde samozřejmě o hyperbolu, vždyť vím, že budoucnost na krásném IT úhoru je otevřena právě pro všechny ty pilné dělníky nové éry, kteří ochotně vygooglují různé nesouvislé fragmenty kódu, jež považují za společný komunitní majetek k instantnímu užití, dále zkombinují několik z nebe spadlých frameworků dohromady a jsou patřičně hrdi na to, že po dlouhé praxi znají alespoň přibližný význam poloviny klíčových slov v C# či Javě. Tedy pro ty, kterým třeba kontravariance v programovacím jazyce nikdy chybět nebude.
Friday, 24 April 2009 11:57:02 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework
Windows Mobile formulář přes celý displej - v nativním kódu
Na MSDN fórech jsem si všiml, že se vícekrát objevil dotaz, jak v nativním kódu vytvořit okno přes celou obrazovku, které se bude chovat jako formulář v Compact .Net Framework aplikaci při nastavení vlastnosti WindowState = Maximized.
API SHFullScreen sice přepne okno do celoobrazovkového režimu, ale při zobrazení SIPu se opět objeví taskbar. Při skrytí SIPu se okno vrátí do celoobrazovkového režimu. První, co mě napadlo, je skrýt samotný taskbar. Idea dobrá, mohli jsme mít jednoduché řešení, ale autoři Windows Mobile jako již tradičně řekli ne.
Po několika pokusech jsem zjistil, že jediné použitelné řešení představuje změna pozice a velikosti formuláře vždy, když odchytím zprávu WINDOWPOSCHANGED. To celé je korunováno opakovaným voláním SHFullScreen. Na řešení v Compact .Net Frameworku jsem se nedíval, abych si nekazil radost z vyřešeného úkolu, takže netuším, zda autoři CNF používají ještě nějaký další trik.
Níže naleznete příklad, který je založen na výchozí šabloně Smart Device Windows API projektu. Zajímavé části jsou zvýrazněny tučně. Tento postup lze samozřejmě jednoduše použít ve WTL nebo MFC.
Tady se ještě zeptám:
1) Jsou alespoń pro někoho z Vás tyto tipy/FAQ zajímavé? Já sám miluji přecházení mezi nativním a “managed” kódem, ale asi nemá smysl, abych tyto tipy psal na blog, jestliže o nativní kód (již) nikdo nestojí.Potom by stačílo, abych je nechal utopeny ve fóru o mobilních zařízeních, kde poslouží podobným individuím jako jsem já. V zásobě mám například často kladený dotaz, jak ve Windows Mobile dialogu zachytit WM_KEY zprávy. I když sám si stále programování v (Compact) .Net frameworku bez dobré znalosti nativního kódu nedovedu představit - což je v roce 2009 možná tristní a nečekaná zpráva.
2) A obecnější dotaz – zajímají někoho z vás tipy pro Windows Mobile/Compact .Net Framework? Pro mě, jak asi tušíte, je programování pro WIN Mobile zařízení potěšení, a proto se podobné tipy objevují i na blogu, který píšu hlavně pro zábavu. I když většinu času jsem nyní strávil vývojem v Silverlightu, WPF, WCF a léčením roztomilých neštovic na zpočátku krásné tváři Linq2SQL, což znamená, že se na blogu se objeví i další témata, která se budou točit kolem návrhu různých typů aplikací a jako bonus odhalíme nejčastější průšvihy spojené s anemickými modely (i model-view-viewmodely )aplikací.
// FullScreen.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include "FullScreen.h"
#define MAX_LOADSTRING 100
// Global Variables:
HINSTANCE g_hInst; // current instance
HWND g_hWndMenuBar; // menu bar handle
RECT usedRect;
// Forward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE, LPTSTR);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
void MakeFullScreen();
void MakeFullScreen(HWND hWnd)
{
SetRect(&usedRect, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
LONG windowWidth = usedRect.right - usedRect.left;
LONG windowHeight = usedRect.bottom - usedRect.top;
MoveWindow(hWnd,
usedRect.left,
usedRect.top,
windowWidth,
windowHeight,
FALSE);
//SIPINFO info;
//info.cbSize = sizeof(info);
//ZeroMemory(&info, sizeof(info));
//info.rcVisibleDesktop = usedRect;
//
//if (!::SipSetInfo(&info))
//{
// int error = GetLastError();
// return FALSE;
//}
SHFullScreen(hWnd, SHFS_HIDETASKBAR | SHFS_HIDESTARTICON);
}
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
// Perform application initialization:
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable;
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FULLSCREEN));
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
// COMMENTS:
//
ATOM MyRegisterClass(HINSTANCE hInstance, LPTSTR szWindowClass)
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_FULLSCREEN));
wc.hCursor = 0;
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = szWindowClass;
return RegisterClass(&wc);
}
//
// FUNCTION: InitInstance(HINSTANCE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
TCHAR szTitle[MAX_LOADSTRING]; // title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // main window class name
g_hInst = hInstance; // Store instance handle in our global variable
// SHInitExtraControls should be called once during your application's initialization to initialize any
// of the device specific controls such as CAPEDIT and SIPPREF.
SHInitExtraControls();
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_FULLSCREEN, szWindowClass, MAX_LOADSTRING);
//If it is already running, then focus on the window, and exit
hWnd = FindWindow(szWindowClass, szTitle);
if (hWnd)
{
// set focus to foremost child window
// The "| 0x00000001" is used to bring any owned windows to the foreground and
// activate them.
SetForegroundWindow((HWND)((ULONG) hWnd | 0x00000001));
return 0;
}
if (!MyRegisterClass(hInstance, szWindowClass))
{
return FALSE;
}
hWnd = CreateWindow(szWindowClass, szTitle, WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
// When the main window is created using CW_USEDEFAULT the height of the menubar (if one
// is created is not taken into account). So we resize the window after creating it
// if a menubar is present
if (g_hWndMenuBar)
{
RECT rc;
RECT rcMenuBar;
GetWindowRect(hWnd, &rc);
GetWindowRect(g_hWndMenuBar, &rcMenuBar);
rc.bottom -= (rcMenuBar.bottom - rcMenuBar.top);
MoveWindow(hWnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, FALSE);
}
ShowWindow(hWnd, nCmdShow);
//MakeFullScreen(hWnd);
UpdateWindow(hWnd);
return TRUE;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
LPWINDOWPOS pos;
static SHACTIVATEINFO s_sai;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_HELP_ABOUT:
DialogBox(g_hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, About);
break;
case IDM_OK:
SendMessage (hWnd, WM_CLOSE, 0, 0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_CREATE:
SHMENUBARINFO mbi;
memset(&mbi, 0, sizeof(SHMENUBARINFO));
mbi.cbSize = sizeof(SHMENUBARINFO);
mbi.hwndParent = hWnd;
mbi.nToolBarId = IDR_MENU;
mbi.hInstRes = g_hInst;
if (!SHCreateMenuBar(&mbi))
{
g_hWndMenuBar = NULL;
}
else
{
g_hWndMenuBar = mbi.hwndMB;
}
// Initialize the shell activate info structure
memset(&s_sai, 0, sizeof (s_sai));
s_sai.cbSize = sizeof (s_sai);
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
CommandBar_Destroy(g_hWndMenuBar);
PostQuitMessage(0);
break;
case WM_WINDOWPOSCHANGED:
pos = (LPWINDOWPOS) lParam;
if ((pos->cx != usedRect.right - usedRect.left) ||
(pos->cy != usedRect.bottom - usedRect.top))
{
MakeFullScreen(hWnd);
}
break;
case WM_ACTIVATE:
// Notify shell of our activate message
SHHandleWMActivate(hWnd, wParam, lParam, &s_sai, FALSE);
break;
case WM_SETTINGCHANGE:
SHHandleWMSettingChange(hWnd, wParam, lParam, &s_sai);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
{
// Create a Done button and size it.
SHINITDLGINFO shidi;
shidi.dwMask = SHIDIM_FLAGS;
shidi.dwFlags = SHIDIF_DONEBUTTON | SHIDIF_SIPDOWN | SHIDIF_SIZEDLGFULLSCREEN | SHIDIF_EMPTYMENU;
shidi.hDlg = hDlg;
SHInitDialog(&shidi);
}
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK)
{
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break;
case WM_CLOSE:
EndDialog(hDlg, message);
return TRUE;
}
return (INT_PTR)FALSE;
}
Tuesday, 14 April 2009 15:04:43 (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Nativní kód