BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Notícias O futuro do C#: Contratos de métodos

O futuro do C#: Contratos de métodos

Favoritos

Por muitos anos, os desenvolvedores puderam adicionar contratos de métodos (method-level contracts) por meio do projeto de pesquisa Code Contracts. Entretanto, esta abordagem apresenta alguns problemas. Ela usa uma sintaxe imperativa que é onerosa e possui baixo suporte das ferramentas. Para usar o contrato na biblioteca e na aplicação, é necessário executar um processo de pós compilação. De forma geral, ele é um projeto interessante, mas necessita de um compilador de primeira classe e de sintaxe de suporte para ser útil.

A proposta 119, contratos de métodos tem como objetivo oferecer este suporte. De forma similar a restrições genéricas, pré e pós condições são listadas entre a assinatura do método e seu corpo. A seguir, temos um código de exemplo:

public int Insert(T item, int index)
	requires index >= 0 && index <= Count
	ensures return >= 0 && return < Count
{ … }

Existem três novas palavras reservadas nesta proposta. Uma instrução que se inicia com "requires" lida como pré condições. Ela provavelmente será mais utilizada para verificar parâmetros, mas em teoria poderia também verificar o estado do próprio objeto. A palavra reservada "ensures" é usada para definir pós condições. Ela vai reutilizar a palavra reservada "return" para se referir ao resultado de uma chamada de método.

Falha rápida versus exceções

Assim, como o Code Contracts, esta proposta foi originalmente escrita para adotar a estratégia de falhar rápido. Esta é uma forma bastante agressiva de execução de contratos de código, em que qualquer violação encerra a aplicação imediatamente. Usando este modelo, desenvolvedores que prefiram usar exceções devem indicar isto manualmente:

public int Insert(T item, int index)
	requires index >= 0 && index <= Count
    	else throw new ArgumentOutOfRangeException(nameof(index))
	ensures return >= 0 && return < Count
{ … }

Esta parte da proposta está sendo debatida com argumentos contrários e a favor de sua utilização.

Segundo Nathan Jervis:

É possível conhecer quais partes do programa não são afetadas e são seguras para continuar a execução. Pode ser que existam situações nas quais se está escrevendo código de missão crítica e talvez queira falhar rápido, mas não creio que seja impossível saber qual parte do seu programa falhou.

É simplesmente ridículo assumir que a ação correta é sempre encerrar o processo imediatamente. Se o Microsoft Word tiver um bug ao salvar em um caminho da rede, por causa de um problema de código, gostaríamos que a aplicação fosse imediatamente encerrada? Não, gostaríamos que o arquivo fosse salvo em algum local temporário, que o erro fosse notificado ao usuário, que o problema fosse armazenado em logs e oferecer a possibilidade de tentar restaurar o arquivo da próxima vez que ele for carregado.

HaloFour compartilha do mesmo sentimento:

Acho que é absolutamente ingênuo que uma violação de contrato utilizada para validação de argumento cause o encerramento completo do processo. Implementar desta forma é uma maneira infalível de garantir que a funcionalidade nunca vai ser usada, pelo menos por ninguém que esteja escrevendo algum código não exotérico. O ponto de discussão desta funcionalidade é a validação de argumentos e isto é certamente algo a partir do qual o programa pode se recuperar de alguma maneira. E francamente, se isto não for possível, o código que o invocou pode tomar a decisão de não capturar a exceção. É assim que todas as implementações de contratos de código que já vi funcionam, tanto em .NET como em qualquer outra linguagem hoje em dia.

David Nelson referenciou o trabalho no Code Contracts:

Tendo trabalhado anteriormente no Code Contracts, ficamos ciente do debate que aconteceu lá sobre falhar rápido ou não. A equipe do Code Contracts tentou convencer a comunidade por meses (talvez anos?) que falhar rápido era a coisa certa a se fazer, mas não obteve sucesso. Tendo a oportunidade de sentir os efeitos de uma decisão tão equivocada, certamente não a apoiaria. Fui uma das pessoas denunciando os absurdos da abordagem de falhar rápido e vou continuar a fazer isso agora.

Mais a frente em sua argumentação, Nelson lista explicitamente os problemas que falhar rápido pode causar:

1) Como lidar com o log de erros? O Watson simplesmente não é suficiente. A grande maioria de aplicações .NET não o utilizam por causa das informações limitadas e arcanas que ele provê e pela dificuldade em acessar estas informações. Toda aplicação .NET que encontro usa o seu próprio esquema de log.

2) É apropriado encerrar um servidor web de produção, servindo milhões de usuários no mundo todo por causa de um bug de lógica inócuo em algum cenário específico de um usuário em particular?

3) O que acontece quando um teste unitário viola um contrato de código? Encerra-se o processo de execução do teste unitário?

4) Se um erro de programação deve encerrar imediatamente o processo, por que qualquer outra condição de erro em .NET deveria lançar uma exceção? Por que NullReferenceException existe: o processo não deveria simplesmente encerrar? Por que uma falha de compilação Just In Time (JIT) de um método (o que certamente indica um problema muito mais sério que uma violação de contrato) lança uma exceção ao invés de encerrar o processo?

Aaron Dandy gostaria de ver duas opções disponíveis:

Certamente utilizaria a abordagem de falhar rápido, mas gostaria de ser capaz de usá-la sobre minha interface privada. Realmente gostaria de usar exceções para a interface pública. Sinto que se um usuário do meu código decide alimentar o contexto do sistema com exceções, essa é uma escolha dele e ele (e implicitamente seus usuários) devem lidar com as consequências.

Esse é um conceito que HaloFour concorda:

Preferiria ver os contratos de métodos lançar exceções (pelo menos para a cláusula "requires") e adicionar uma nova palavra reservada, "assert", para habilitar a abordagem de falhar rápido se qualquer condição não for atingida.

Tipos de exceções

A parte simples desta proposta são as exceções de argumentos. O compilador pode facilmente transformar uma simples cláusula "requires" em uma exceção ArgumentNullException ou ArgumentOutOfRange. Se esta cláusula requires inspecionar o estado de um objeto, ele poderia lançar uma exceção InvalidOperationException. Mas e se a cláusula "requires" inspecionar as duas situações? Neste caso, determinar qual exceção lançar pode se tornar algo muito complicado.

Também existe um problema com a exceção ObjectDisposedException. Não existe um padrão para representar um objeto descartado. Ao invés disso, existe uma convenção bem fraca na qual um campo booleano chamado _disposed ou m_IsDisposed ou algo deste tipo deve ser verificado. Isto é importante porque frequentemente pode-se recuperar da exceção InvalidOperationException por meio da alteração do estado do objeto, enquanto não é possível se recuperar da exceção ObjectDisposedException.

Por outro lado, é necessário que exista uma exceção que indique que a cláusula "ensures" falhou. De forma distinta da cláusula requires, uma falha na cláusula "ensures" sempre significa que existe um bug interno ao método.

Localização

Assumindo que uma abordagem baseada em exceções foi utilizada, a próxima questão que surge é sobre localização. Para simples verificações de argumentos, o compilador poderia facilmente gerar texto em inglês para exceções de argumentos. Mas o que acontece quando estas exceções precisarem ser localizadas para outras linguagens? Ao utilizar a sintaxe abreviada na qual o argumento de exceção não precisa ser explicitamente indicado, haverá a necessidade de existir um canal no qual essa informação pode ser adicionada. Ou talvez a localização exigirá a sintaxe detalhada.

Enumeradores e Contratos

Até o momento, os contratos tem se constituído como meros aditivos. Na proposta de Fabian Schmied, o compilador pode eliminar a necessidade de adicionar uma instrução de retorno que nunca será executada:

public enum MyEnum { One, Two, Three };

public string GetText (MyEnum myEnum)
requires defined(myEnum)
{
	switch (myEnum)
	{
    	case One: return "Single";
    	case Two: return "Pair";
    	case Three: return "Triple";
	}   
	//Nenhum erro sobre instruções de retorno ausentes; todos caminhos de código cobertos.
}

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT