Porque os operadores em C# são sempre static?

02/28/2008 15:41:00 By Felipe Pessoto

Porque os operadores em C# são sempre static? No C++ um operador sobrecarregado pode ser implementado por métodos static, instanciados ou virtual. Há alguma razão para esta regra no C#?

Antes de irmos direto ao assunto, há um ponto que vale a pena discutirmos um pouco. Raymond Chen imediatamente disse que tínhamos que voltar um pouco para o passado do C#. O design da linguagem C# não é um processo de subtração. C# não é um Java/C++ com as partes que não se ligavam removidas.

A questão que devemos nos perguntar sobre uma característica potencial da linguagem é "os benefícios trazidos irão justificar os custos?". E os custos são consideravelmente mais que somente os dólares gastos com designing, desenvolvimento, testes, documentação e manutenção. Há mais pequenos custos, como, este recurso tornará mais difícil a troca do algoritmo de inferência de tipo no futuro? Será que isso nos levará a um mundo onde não poderemos fazer alterações sem introduzir incompatibilidades? E por ai vai...

Neste caso específico, os benefícios são pequenos. Se você quer ter um operador virtual sobrecarregado no C# você pode construir um contornando as partes static. Por exemplo:

public class B {
public static B operator+(B b1, B b2) { return b1.Add(b2); }
protected virtual B Add(B b2) { // ...

E você terá ele. Então, os benefícios são pequenos. Mas os custos são grandes. Operadores instanciados no estilo C++ são esquisitos. Por exemplo, eles quebram a simetria. Se você definir um operador + que recebe um C e um int, então c+2 é válido mas 2+c não é, isso que rompe nosso conceito sobre como o operador de adição deve se comportar.

Similarmente, com operadores virtual no C++, o argumento à esquerda é o que serve de parâmetro para chamada virtual. Então novamente, nós temos uma assimetria entre os lados esquerdo e direito. Realmente o que você quer para a maioria dos operadores binários é que sejam comutativos -- você quer que o operador seja virtualizado nos tipos de ambos argumentos, não somente o do lado esquerdo. Mas nem o C# nem C++ suportam comutação nativamente. (Muitos problemas do mundo real devem ser resolvidos se tivermos comutação; por uma coisa, o visitante padrão torna-se trivial.)

E finalmente, no C++ você pode somente definir um operador sobrecarregado sobre um tipo que não seja um ponteiro, isto é um só pode definir para tipos por valor. Isto quer dizer que quando você ver c+2 e reescrever como c.operator+(2), você está garantindo que c não é um ponteiro nulo porque ele nunca será um ponteiro! C# também faz a distinção entre valores e referência, mas seria muito estranho se um operador instanciado sobrecarregado fosse apenas definido sobre tipos por valor "non-nullable", e também seria estranho se c+2 pudesse gerar uma exceção "null reference".

Por isso foi decidido não adicionar operadores sobrecarregados por instância ou virtual no C#.

Covariância e Contravariância em C#, Parte Três: Variância em Grupo de Métodos

02/13/2008 08:45:00 By Felipe Pessoto

Anteriormente discutimos como a covariância numa array não funciona corretamente no C# (e Java, assim como uma série de outras linguagens). Agora, um tipo de variância válida suportada no C# 2.0: conversões de grupo de métodos em delegates. Este é um tipo mais complicado de variância, por isso vou explicar com mais detalhes.

Suponha que você tem um método que retorna um objeto Girafa.

static Girafa CriarGirafa() { }

E que você tem um delegate representando uma função que não recebe argumentos e retorna um Animal. Isto é, Func<animal>. Deveria esta conversão ser válida?

Func<animal> func = CriarGirafa;

Ao invocar Func é esperado que um Animal seja retornado. A função atual chamada pelo delegate sempre retorna uma Girafa, a qual é um Animal, então o invocador da função nunca recebe nada que ele não seja capaz de lidar. Não há problemas no sistema de tipo aqui. Portanto nós podemos criar métodos para delegate usando conversões covariantes nos seus tipos de retorno.


Agora vamos supor que você tem dois métodos, um que recebe Girafa e um que recebe um Animal:

void Foo(Girafa g) { }
void Bar(Animal a) { }

e um delegate que retorna vazio e recebe um argumento do tipo Mamifero:

Action<Mamifero> action1 = Foo; // inválido
Action<Mamifero> action2 = Bar; // válido

Por quê a primeira atribuição é inválida? Porque quem invocar o action1 pode passar por exemplo um tipo Tigre(já que seria derivado de Mamifero), mas Foo não pode receber um Tigre, somente uma Girafa(e seus derivados)! A segunda atribuição é válida porque Bar pode receber qualquer Animal.

No exemplo anterior preservamos a direção de atribuição: Girafa é menor que Animal, então o método que retorna Girafa é menor que o delegate que retorna um Animal. Neste exemplo, nós revertemos a direção de atribuição: Mamifero é menor que Animal, então o método que recebe Animal é menor que o delegate que recebe um Mamifero. Porque a direção está invertida, conversões de grupo de métodos para delegate são contravariantes nos tipos de seus argumentos.

Percebe que tudo falado acima aplica-se apenas em tipos por referência. Nunca diga algo como "Bem, todo int cabe em um long, então um método que retorna um int pode ser atribuído para uma variável do tipo Func<long>".

Acesso à Banco de Dados III Usando Parâmetros

01/15/2008 11:32:00 By Felipe Pessoto

Agora que já sabemos como executar comando no banco de dados, vamos aprender à executá-los usando parâmetros.

Os parâmetros são como variáveis que contém valores que são enviados e retornados do banco de dados, eles têm tipo e um conteúdo assim como as variáveis que usamos nos nossos programas.

Para definirmos o tipo do parâmetro usamos o enum SqlDbType que têm os tipos usados no SQL Server(para o Oracle temos o OracleType). Tudo isso será melhor abordado adiante, faremos um exemplo prático.

Geralmente usamos parâmetros quando queremos mudar algo rapidamente e especificamente, na maioria das vezes na cláusula WHERE. Outra vantagem é a segurança proporcionada, pois ao deixar de fazer concatenação de strings para formar uma Query SQL eliminamos o risco de um SQL Injection. A seguir um exemplo de uma Query usando parâmetro:

SELECT nome, idade, cidade FROM Clientes WHERE cd_cliente= @Codigos

Os parâmetros podem ser de entrada, saída ou entrada e saída, definido na propriedade Direction. Por padrão o parâmetro é definido como de entrada.

Para criarmos um parâmetro declaramos uma instância da classe Parameter, e atribuímos o seu nome e tipo de dado, que deve coincidir com o esperado pela fonte de dados, exemplo:

SqlParameter MeuParametro = new SqlParameter();
MeuParametro.ParameterName = "@Codigo";
MeuParametro.SqlDbType = SqlDbType.Int;

//Exemplo definindo como parâmetro de saída
MeuParametro.Direction.ParameterDirection.Output;

Depois que temos nosso parâmetro, vamos adicionar ele ao objeto Command. Os objetos Command tem uma propriedade chamada Parameters, que representa uma coleção de parâmetros. Então usamos o método Add:

MeuComando.Parameters.Add(MeuParametro);

Falta apenas, é claro, definirmos o valor do nosso parâmetro(caso seja de entrada ou de entrada e saída):

MeuParametro.Value = 50;

A partir daí não muda mais nada, podemos executar o nosso comando como fizemos anteriormente:

SqlDataReader reader = MeuComando.ExecuteReader();

Acesso à Banco de Dados II Executando Comandos

01/10/2008 09:11:00 By Felipe Pessoto

Para se executar comando SQL ou Stored Procedures em um banco de dados, usamos objetos Command.

Assim como acontece com as conexões temos um objeto Command específico para cada Data Provider. Se você estiver usando um SqlConnection para se conectar ao SQL Server deve também usar SqlCommand para executar os comandos, se estiver usando um Provider OLE DB, o OleDbCommand será usado e assim por adiante.

As propriedades principais dos objetos Command são CommandText, CommandType e Connection.
O CommandType é para você especificar que tipo de comando será usado, se é uma Query SQL ou Stored Procedure. Exemplo:

SqlCommand.CommandType = CommandType.Text;
SqlCommand.CommandType = CommandType.StoredProcedure;

E então deve especificar na propriedade CommandText a Query SQL ou o nome da Stored Procedure.
A propriedade Connection é usada pra saber em qual conexão aquela instrução será executada. Você atribuir uma conexão previamente criada à essa propriedade. Exemplos:

 

SqlCommand MeuComando = new SqlCommand();
MeuComando.Connection = NorthwindConnection;
MeuComando.CommandType = CommandType.Text;
MeuComando.CommandText = "SELECT * FROM Customers";
 
SqlCommand TopTenCommand = new SqlCommand();
TopTenCommand.Connection = NorthwindConnection;
TopTenCommand.CommandType = CommandType.StoredProcedure;
TopTenCommand.CommandText = "Ten Most Expensive Products";

Depois que você tiver configurado seu objeto Command, deve executa-lo. Para isso temos 3 possibilidades, executar um comando que não retorna dados(ExecuteNonQuery), que retorna os dados linha por linha(ExecuteReader) ou que retorna apenas um valor(ExecuteScalar, primeira coluna da primeira linha). Exemplo de ExecuteScalar e ExecuteNonQuery:

 
SqlCommand ExecuteScalarCommand = new SqlCommand();
ExecuteScalarCommand.Connection = ObjetodeConexao;
ExecuteScalarCommand.CommandType = CommandType.Text;
ExecuteScalarCommand.CommandText = "SELECT Count(*) FROM Customers";

ExecuteScalarCommand.Connection.Open();
int CustomerCount = (int)ExecuteScalarCommand.ExecuteScalar();
ExecuteScalarCommand.Connection.Close();

SqlCommand CreateTableCommand = new SqlCommand();
CreateTableCommand.Connection = NorthwindConnection;
CreateTableCommand.CommandType = CommandType.Text;
CreateTableCommand.CommandText = "CREATE TABLE Clientes (" +
"[ClienteID] [int] IDENTITY(1,1) NOT NULL, " +
"[Nome] [nvarchar](50) NULL, )";

CreateTableCommand.ExecuteNonQuery();​
 

 

Para o ExecuteReader temos um pouco mais de trabalho. Criamos um objeto DataReader para ler a tabela retornada e executamos seu método Read(). Um ponteiro estará apontando para a primeira linha dessa tabela, e cada vez que executamos o Read() é retornado True se ouver mais linhas e o ponteiro é passado para a seguinte. Exemplo de ExecuteReader:

 

ExecuteSqlCommand.CommandText = "SELECT * FROM Customers;";
SqlDataReader reader = ExecuteSqlCommand.ExecuteReader();
 
StringBuilder results = new StringBuilder();
while (reader.Read())
{
for (int i = 0; i < reader.FieldCount; i++)
{
results.Append(reader[i].ToString() + "\t");
}
results.Append(Environment.NewLine);
}​

 

E aqui terminamos mais uma parte da série Acesso à Banco de Dados, aprendendo como obter resultados de um Banco de Dados

Acesso à Banco de Dados I Conexão

01/09/2008 16:39:00 By Felipe Pessoto

Vou dar uma pausa na série Covariância e Contravariância pra falar um pouco sobre conceitos Básicos de Acesso a Banco de Dados.
Tem muita coisa que quero escrever, mas vai depender do meu tempo livre, e ainda tenho que terminar o livro do exame 70-526(já foram 5cap.)
Outro assunto muito interessanta é o LINQ, ele facilita muito o acesso ao banco de dados e dá mais confiança já que é tudo verificado em tempo de compilação. Mas isso fica pra uma próxima oportunidade.

Continuando, vamos estudar um pouco os objetos de conexão.

Primeiro, o que é um objeto de conexão?

O objeto de conexão é um "canal" que seus comandos e querys vão usar para se comunicar ao banco e dados. Ele também vai determinar se você vai usar Pool de Conexões(veremos posteriormente).

O Visual Studio permite que você gerencie suas conexões pela janela Server Explorer. As conexões adicionadas pelo Server Explorer ficam acessíveis à todos os projetos que você criar.
Para adicionar uma nova conexão é muito simples, abaixo mostro como adicionar um arquivo MDB do Access:



Quando você clicar em OK a nova conexão já será adicionada ao Server Explorer.
Pra adicionar uma conexão no SQL Server é só clicar no botão Change e escolher o Microsoft SQL Server. Alguns itens serão adicionados à janela, como a opção se usar autenticação integrada ou um usuário do SQL Server(explicarei adiante).

Outra forma de se criar uma conexão é via código. Nativamente o ADO.Net vem com objetos de conexão para SQL Server(SqlConnection), Oracle(OracleConnection), ODBC(OdbcConnection) e OLE DB(OleDbConnection).
Para usar estes objetos você vai precisar configurar a ConnectionString, indicando o DataSource, Database, Credenciais de Segurança, etc.

Exemplo:

SqlConnection ConnectionSql = new SqlConnection (@"Data Source=.\sqlexpress;Initial Catalog=Northwind;Integrated Security=True");

Neste exemplo conectamos à instância sqlexpress da máquina local, usando o banco de dados Northwind e segurança integrada.

Quando usamos Segurança Integrada, o usuário atual do Windows será usado. Este tipo de autenticação é a mais recomendada, então use-a sempre que possível.

Depois de criada, podemos abrir nossa conexão usando o método Open():

ConnectionSql.Open();

E para fecharmos usamos o método Close():

ConnectionSql.Close();

Sempre que uma conexão muda de estado(como abrir uma conexão fechada), você pode ser notificado usando o evento StateChange:

private void ConnectionSql_StateChange(object sender, StateChangeEventArgs e)
{
Label1.Text = e.CurrentState.ToString();
}

Outro evento interessante é o InfoMessage, ele é disparado quando algum aviso não crítico é recebido do banco de dados.

private void ConnectionSql_InfoMessage(object sender, SqlInfoMessageEventArgs e)
{
MessageBox.Show(e.Message);
}

Connection Pools

O Pool de Conexões tem um papel muito importante na questão da performance. Cada vez que é instanciado uma nova conexão é usado muito tempo de processamento, então faz sentido guardarmos as conexões por um tempo antes de destrui-las.
As conexões são reutilizadas quando estão no mesmo processo, application domain e string de conexão.
Podemos controlar o comportamento do Pool de Conexões através da string de conexão, vou listar os valores usados:

Connection Lifetime - Quando uma conexão é devolvida ao pool, é verificado à quantos segundos ela foi criada, se ultrapassar o valor definido aqui, a conexão é destruida.
Load Balance Timeout - O tempo mínimo que uma conexão fica na fila antes de ser destruída.
Max Pool Size - O número máximo de conexões que podem ficar na fila para cada string de conexão.
Min Pool Size - O número mínimo de conexões que devem ficar na fila.

Exemplo:
"Data Source=.\sqlexpress;Initial Catalog=Northwind;Integrated Security=True;Min Pool Size=10"

Caso você queira limpar a fila de uma determinada conexão pode usar o método ClearPool, e também é possível limpar o pool de todas as conexões de um determinado providor usando o método ClearAllPools.

Por padrão o Pool de Conexões já vem ativado, então você não precisa se preocupar em ativa-lo.

Manipulando Erros de Conexão

Para manipular um erro de conexão você usa o try/catch, capturando a exceção SqlException.
Essa classe contém uma coleção SqlErrorCollection na propriedade SqlException.Errors com pelo menos um objeto SqlError.

Exemplo:

try
{
connection.Open();
}
catch (SqlException ex)
{
 
string mensagem = "";
 
foreach (SqlError ConnectionError in ex.Errors)
{
mensagem += ConnectionError.Message + " (error: " + ConnectionError.Number.ToString() + ")" + Environment.NewLine;
 
if (ConnectionError.Number == 18452)
{
MessageBox.Show("Dados de Login Inválidos");
}
}
MessageBox.Show(mensagem);
 
}

Procurando SQL Servers disponíveis na rede

A classe SqlDataSourceEnumerator tem uma propriedade chamada Instance que pode ser usada para buscar por servidor SQL Server na rede. Basta executar o método GetDataSources() e será retornado um DataTable contendo informações sobre cada SQL Server encontrado.
Os servidores encontrados vão depender das configurações da rede, firewall, etc.
O DataTable retornado pelo GetDataSources() conterá as seguintes colunas:

ServerName - Nome do servidor
InstanceName - Nome da instância do SQL Server
IsClustered - Indica se o servidor é parte de um cluster
Version - Versao do SQL Server

Exemplo:

SqlDataSourceEnumerator instance = SqlDataSourceEnumerator.Instance;
MeuGrid.DataSource = instance.GetDataSources();

Aqui acabamos a parte sobre Conexões. Espero escrever a próxima parte em breve.
Até a próxima.