Quais são as diferenças entre os genéricos em C # e Java ... e os modelos em C ++? [fechadas]


203

Eu uso principalmente Java e genéricos são relativamente novos. Eu continuo lendo que o Java tomou a decisão errada ou que o .NET tem implementações melhores etc. etc.

Então, quais são as principais diferenças entre C ++, C # e Java em genéricos? Prós / contras de cada um?

Respostas:


364

Vou acrescentar minha voz ao barulho e tentar esclarecer as coisas:

Os C # Generics permitem declarar algo assim.

List<Person> foo = new List<Person>();

e o compilador impedirá que você coloque coisas que não estão Personna lista.
Nos bastidores, o compilador C # está apenas List<Person>inserindo o arquivo dll .NET, mas em tempo de execução o compilador JIT cria e constrói um novo conjunto de códigos, como se você tivesse escrito uma classe de lista especial apenas para conter pessoas - algo como ListOfPerson.

O benefício disso é que o torna muito rápido. Não há conversão ou qualquer outra coisa, e como a dll contém as informações de que é uma lista Person, outro código que a analisa mais tarde usando a reflexão pode dizer que ela contém Personobjetos (para que você entenda bem o sentido).

A desvantagem disso é que o código C # 1.0 e 1.1 antigo (antes de adicionarem genéricos) não entende esses novos List<something>, portanto, você deve converter manualmente as coisas novamente para antigas simples Listpara interoperar com elas. Esse não é um grande problema, porque o código binário do C # 2.0 não é compatível com versões anteriores. A única vez que isso acontecerá é se você estiver atualizando algum código C # 1.0 / 1.1 antigo para C # 2.0

Os Java Generics permitem declarar algo assim.

ArrayList<Person> foo = new ArrayList<Person>();

Na superfície, parece o mesmo, e meio que é. O compilador também impedirá que você coloque coisas que não sãoPerson na lista.

A diferença é o que acontece nos bastidores. Ao contrário do C #, o Java não cria um especial ListOfPerson- apenas usa o antigo simples ArrayListque sempre esteve em Java. Quando você tira as coisas da matriz, a Person p = (Person)foo.get(1);dança do elenco habitual ainda precisa ser feita. O compilador está poupando as teclas pressionadas, mas a velocidade de execução / conversão ainda é incorrida como sempre.
Quando as pessoas mencionam "Eliminação de tipo", é sobre isso que estão falando. O compilador insere os modelos para você e depois 'apaga' o fato de que ele deve ser uma lista Personnão apenasObject

O benefício dessa abordagem é que o código antigo que não entende os genéricos não precisa se preocupar. Ainda está lidando com o mesmo velhoArrayList sempre. Isso é mais importante no mundo java porque eles queriam oferecer suporte à compilação de código usando o Java 5 com genéricos e a execução em JVMs 1.4 ou anteriores, que a Microsoft decidiu deliberadamente não se incomodar.

A desvantagem é a velocidade atingida que mencionei anteriormente, e também porque não há ListOfPersonpseudo-classe ou algo parecido nos arquivos .class, código que o analisa mais tarde (com reflexão ou se você retirá-lo de outra coleção). onde foi convertido Objectou assim por diante) não pode dizer de forma alguma que deve ser uma lista contendo apenas Persone não apenas qualquer outra lista de matrizes.

Modelos C ++ permitem que você declare algo parecido com isto

std::list<Person>* foo = new std::list<Person>();

Parece C # e Java genéricos e fará o que você acha que deve fazer, mas nos bastidores coisas diferentes estão acontecendo.

Ele tem o mais em comum com os genéricos de C #, na medida em que cria informações especiais, em pseudo-classesvez de apenas jogar fora as informações de tipo, como o java, mas é uma chaleira de peixe totalmente diferente.

C # e Java produzem saída projetada para máquinas virtuais. Se você escrever algum código que contenha uma Personclasse, em ambos os casos algumas informações sobre uma Personclasse serão inseridas no arquivo .dll ou .class, e a JVM / CLR fará as coisas necessárias.

C ++ produz código binário x86 bruto. Tudo não é um objeto e não há uma máquina virtual subjacente que precise saber sobre umPerson classe. Não há boxe ou unboxing, e as funções não precisam pertencer a classes, ou mesmo qualquer coisa.

Por causa disso, o compilador C ++ não impõe restrições ao que você pode fazer com modelos - basicamente qualquer código que você possa escrever manualmente, você pode obter modelos para escrever para você.
O exemplo mais óbvio é adicionar coisas:

Em C # e Java, o sistema genérico precisa saber quais métodos estão disponíveis para uma classe e deve passar isso para a máquina virtual. A única maneira de dizer isso é codificando a classe real ou usando interfaces. Por exemplo:

string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }

Esse código não será compilado em C # ou Java, porque ele não sabe que o tipo Trealmente fornece um método chamado Name (). Você precisa dizer - em C # assim:

interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }

E você precisa se certificar de que as coisas que você passa para addNames implementam a interface IHasName e assim por diante. A sintaxe do java é diferente ( <T extends IHasName>), mas sofre dos mesmos problemas.

O caso "clássico" para esse problema está tentando escrever uma função que faz isso

string addNames<T>( T first, T second ) { return first + second; }

Você não pode realmente escrever esse código porque não há maneiras de declarar uma interface com o +método nele. Você falhou.

C ++ sofre de nenhum desses problemas. O compilador não se importa em passar tipos para nenhuma VM - se os dois objetos tiverem uma função .Name (), ele será compilado. Se não, não vai. Simples.

Então, aí está :-)


8
A pseudo-classe gerada para tipos de referências em C # compartilha a mesma implementação, para que você não obtenha exatamente o ListOfPeople. Confira blogs.msdn.com/ericlippert/archive/2009/07/30/…
Piotr Czapla

4
Não, você não pode compilar o código Java 5 usando genéricos e executá-lo em 1.4 VMs antigas (pelo menos o Sun JDK não implementa isso. Algumas ferramentas de terceiros fazem isso.) O que você pode fazer é usar 1.4 JARs compilados anteriormente de Código 1.5 / 1.6.
finnw

4
Oponho-me à afirmação de que você não pode escrever int addNames<T>( T first, T second ) { return first + second; }em c #. O tipo genérico pode ser restrito a uma classe em vez de uma interface, e existe uma maneira de declarar uma classe com o +operador nela.
Mashmagar

4
@AlexanderMalakhov é não-idiomático de propósito. O objetivo não era educar sobre os idiomas do C ++, mas ilustrar como o mesmo código é tratado de maneira diferente por cada idioma. Esse objetivo teria sido mais difícil de alcançar, quanto mais diferente o código, parece
Orion Edwards

3
@ phresnel Eu concordo em princípio, mas se eu tivesse escrito esse trecho em C ++ idiomático, seria muito menos compreensível para os desenvolvedores de C # / Java e, portanto (acredito), teria feito um trabalho pior ao explicar a diferença. Vamos concordar em discordar sobre este :-)
Orion Edwards

61

O C ++ raramente usa a terminologia "genérica". Em vez disso, a palavra "modelos" é usada e é mais precisa. Modelos descreve uma técnica para obter um design genérico.

Os modelos C ++ são muito diferentes do que C # e Java implementam por dois motivos principais. A primeira razão é que os modelos C ++ não apenas permitem argumentos do tipo tempo de compilação, mas também argumentos de valor const em tempo de compilação: os modelos podem ser dados como números inteiros ou mesmo como assinaturas de funções. Isso significa que você pode fazer algumas coisas bastante divertidas em tempo de compilação, por exemplo, cálculos:

template <unsigned int N>
struct product {
    static unsigned int const VALUE = N * product<N - 1>::VALUE;
};

template <>
struct product<1> {
    static unsigned int const VALUE = 1;
};

// Usage:
unsigned int const p5 = product<5>::VALUE;

Esse código também usa o outro recurso distinto dos modelos C ++, a especialização de modelos. O código define um modelo de classe, productque possui um argumento de valor. Ele também define uma especialização para esse modelo que é usado sempre que o argumento é avaliado como 1. Isso me permite definir uma recursão sobre as definições de modelo. Eu acredito que isso foi descoberto por Andrei Alexandrescu .

A especialização de modelos é importante para C ++, pois permite diferenças estruturais nas estruturas de dados. Modelos como um todo é um meio de unificar uma interface entre tipos. No entanto, embora isso seja desejável, todos os tipos não podem ser tratados igualmente dentro da implementação. Modelos C ++ levam isso em consideração. Essa é a mesma diferença que o OOP faz entre interface e implementação com a substituição de métodos virtuais.

Modelos C ++ são essenciais para seu paradigma de programação algorítmica. Por exemplo, quase todos os algoritmos para contêineres são definidos como funções que aceitam o tipo de contêiner como um tipo de modelo e os tratam de maneira uniforme. Na verdade, isso não está certo: o C ++ não funciona em contêineres, mas em intervalos definidos por dois iteradores, apontando para o início e o final do contêiner. Assim, todo o conteúdo é circunscrito pelos iteradores: begin <= elements <end.

Usar iteradores em vez de contêineres é útil porque permite operar em partes de um contêiner em vez de no todo.

Outro recurso distintivo do C ++ é a possibilidade de especialização parcial para modelos de classe. Isso está relacionado à correspondência de padrões nos argumentos do Haskell e outras linguagens funcionais. Por exemplo, vamos considerar uma classe que armazena elementos:

template <typename T>
class Store {  }; // (1)

Isso funciona para qualquer tipo de elemento. Mas digamos que podemos armazenar ponteiros com mais eficiência do que outros tipos, aplicando algum truque especial. Podemos fazer isso especializando-nos parcialmente para todos os tipos de ponteiros:

template <typename T>
class Store<T*> {  }; // (2)

Agora, sempre que instanciamos um modelo de contêiner para um tipo, a definição apropriada é usada:

Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.

Às vezes, eu desejava que o recurso genérico no .net pudesse permitir que coisas além de tipos fossem usadas como chaves. Se matrizes do tipo valor fizerem parte do Framework (estou surpreso que elas não sejam, de certa forma, dada a necessidade de interagir com APIs mais antigas que incorporam matrizes de tamanho fixo nas estruturas), seria útil declarar um classe que continha alguns itens individuais e, em seguida, uma matriz de valor cujo tamanho era um parâmetro genérico. Assim, o mais próximo possível é ter um objeto de classe que contenha itens individuais e, em seguida, também faça referência a um objeto separado que contenha a matriz.
Supercat

@supercat Se você interagir com a API herdada, a idéia é usar o empacotamento (que pode ser anotado por meio de atributos). O CLR não possui matrizes de tamanho fixo, portanto, ter argumentos de modelo que não sejam do tipo não ajudaria aqui.
Konrad Rudolph

Eu acho que o que acho intrigante é que parece que ter matrizes de tipo de valor de tamanho fixo não deveria ter sido difícil e permitiria que muitos tipos de dados fossem agrupados por referência e não por valor. Embora marechal-por-valor possa ser útil em casos que realmente não podem ser tratados de outra maneira, consideraria marechal-por-ref superior em quase todos os casos em que é utilizável, permitindo que esses casos incluam estruturas com recursos fixos matrizes de tamanho pareciam um recurso útil.
Supercat

BTW, outra situação em que parâmetros genéricos que não sejam do tipo seria útil seria com tipos de dados que representam quantidades dimensionadas. Pode-se incluir informações dimensionais em instâncias que representam quantidades, mas ter essas informações em um tipo permitiria especificar que uma coleção deveria conter objetos representando uma unidade dimensionada específica.
Supercat 6/13


18

Já existem muitas boas respostas sobre quais são as diferenças, então deixe-me dar uma perspectiva um pouco diferente e acrescentar o porquê .

Como já foi explicado, a principal diferença é o apagamento de tipo , ou seja, o fato de o compilador Java apagar os tipos genéricos e eles não acabam no bytecode gerado. No entanto, a pergunta é: por que alguém faria isso? Não faz sentido! Ou faz?

Bem, qual é a alternativa? Se você não implementar genéricos na língua, onde fazer implementa? E a resposta é: na máquina virtual. O que quebra a compatibilidade com versões anteriores.

Por outro lado, o apagamento de tipo permite combinar clientes genéricos com bibliotecas não genéricas. Em outras palavras: o código que foi compilado no Java 5 ainda pode ser implementado no Java 1.4.

A Microsoft, no entanto, decidiu quebrar a compatibilidade com os genéricos. É por isso que o .NET Generics é "melhor" que o Java Generics.

É claro que a Sun não é idiota ou covarde. A razão pela qual eles se destacaram foi que o Java era significativamente mais antigo e mais difundido que o .NET quando eles introduziram genéricos. (Eles foram introduzidos aproximadamente ao mesmo tempo nos dois mundos.) Quebrar a compatibilidade com versões anteriores teria sido uma grande dor.

Dito de outra maneira: em Java, os genéricos fazem parte da linguagem (o que significa que eles se aplicam apenas a Java, não a outras linguagens); no .NET, fazem parte da máquina virtual (o que significa que se aplicam a todas as linguagens, não apenas C # e Visual Basic.NET).

Compare isso com os recursos do .NET, como LINQ, expressões lambda, inferência de tipo de variável local, tipos anônimos e árvores de expressão: esses são todos os recursos de idioma . É por isso que existem diferenças sutis entre VB.NET e C #: se esses recursos fizessem parte da VM, seriam iguais em todos os idiomas. Mas o CLR não mudou: ainda é o mesmo no .NET 3.5 SP1 como no .NET 2.0. Você pode compilar um programa C # que usa LINQ com o compilador .NET 3.5 e ainda executá-lo no .NET 2.0, desde que você não use nenhuma biblioteca do .NET 3.5. Isso não trabalho com os genéricos e .NET 1.1, mas iria trabalhar com Java e Java 1.4.


3
O LINQ é principalmente um recurso de biblioteca (embora o C # e o VB também adicionem açúcar de sintaxe ao lado). Qualquer idioma direcionado ao CLR 2.0 pode obter o uso completo do LINQ simplesmente carregando o assembly System.Core.
Richard Berg

Sim, desculpe, eu deveria ter sido mais claro. LINQ. Eu estava me referindo à sintaxe da consulta, não aos operadores de consulta padrão monádicos, aos métodos de extensão LINQ ou à interface IQueryable. Obviamente, você pode usar aqueles de qualquer linguagem .NET.
Jörg W Mittag

1
Estou pensando em outra opção para Java. Mesmo o Oracle não deseja interromper a compatibilidade com versões anteriores, ainda pode fazer alguns truques do compilador para evitar que as informações de tipo sejam apagadas. Por exemplo, ArrayList<T>pode ser emitido como um novo tipo nomeado internamente com um Class<T>campo estático (oculto) . Desde que a nova versão da lib genérica tenha sido implementada com o código de 1,5 ou mais bytes, ela poderá ser executada em 1.4-JVMs.
Earth Engine

14

Acompanhamento da minha postagem anterior.

Os modelos são um dos principais motivos pelos quais o C ++ falha de maneira tão absurda no intellisense, independentemente do IDE usado. Devido à especialização de modelos, o IDE nunca pode ter certeza se um membro existe ou não. Considerar:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

Agora, o cursor está na posição indicada e é muito difícil para o IDE dizer nesse momento se, e o que, membros a têm. Para outros idiomas, a análise seria simples, mas para C ++, é necessária uma certa avaliação prévia.

Fica pior. E se também my_int_typefossem definidos dentro de um modelo de classe? Agora, seu tipo dependeria de outro argumento de tipo. E aqui, até os compiladores falham.

template <typename T>
struct Y {
    typedef T my_type;
};

X<Y<int>::my_type> b;

Depois de pensar um pouco, um programador concluiria que esse código é o mesmo que o acima: Y<int>::my_typeresolve para int, portanto, bdeve ser do mesmo tipo quea , certo?

Errado. No momento em que o compilador tenta resolver esta declaração, ele ainda não sabe Y<int>::my_type! Portanto, ele não sabe que esse é um tipo. Pode ser outra coisa, por exemplo, uma função membro ou um campo. Isso pode dar origem a ambiguidades (embora não no presente caso), portanto, o compilador falha. Temos que dizer explicitamente que nos referimos a um nome de tipo:

X<typename Y<int>::my_type> b;

Agora, o código compila. Para ver como as ambiguidades surgem dessa situação, considere o seguinte código:

Y<int>::my_type(123);

Essa declaração de código é perfeitamente válida e diz ao C ++ para executar a chamada de função Y<int>::my_type. No entanto, se my_typenão for uma função, mas um tipo, essa instrução ainda seria válida e executaria uma conversão especial (a conversão no estilo de função), que geralmente é uma invocação de construtor. O compilador não pode dizer o que queremos dizer, então temos que desambiguar aqui.


2
Eu concordo bastante. Há alguma esperança, no entanto. O sistema de preenchimento automático e o compilador C ++ devem interagir muito estreitamente. Tenho certeza de que o Visual Studio nunca terá esse recurso, mas as coisas podem acontecer no Eclipse / CDT ou em algum outro IDE baseado no GCC. ESPERANÇA ! :)
Benoit

6

Java e C # introduziram genéricos após o lançamento do primeiro idioma. No entanto, existem diferenças em como as bibliotecas principais foram alteradas quando os genéricos foram introduzidos. Os genéricos do C # não são apenas mágicos do compilador e, portanto, não foi possível gerar classes de bibliotecas existentes sem quebrar a compatibilidade com versões anteriores.

Por exemplo, em Java, o Framework de coleções existente foi completamente genérico . Java não possui uma versão genérica e não genérica herdada das classes de coleções. De certa forma, isso é muito mais limpo - se você precisar usar uma coleção em C #, há muito pouco motivo para usar a versão não genérica, mas essas classes herdadas permanecem no lugar, ocupando a paisagem.

Outra diferença notável são as classes Enum em Java e C #. O Enum do Java tem essa definição de aparência um tanto tortuosa:

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

(veja a explicação muito clara de Angelika Langer sobre exatamente por que isso é assim. Essencialmente, isso significa que o Java pode conceder acesso seguro ao tipo de uma string para o seu valor Enum:

//  Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");

Compare isso com a versão do C #:

//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");

Como o Enum já existia em C # antes da introdução de genéricos no idioma, a definição não pôde ser alterada sem a quebra do código existente. Assim, como coleções, ele permanece nas bibliotecas principais nesse estado herdado.


Mesmo os genéricos do C # não são apenas mágica do compilador, o compilador pode fazer mais mágica para gerar a biblioteca existente. Não há nenhuma razão por que eles precisam para mudar o nome ArrayListpara List<T>e colocá-lo em um novo namespace. O fato é que, se uma classe aparecer no código-fonte, ArrayList<T>ela se tornará um nome de classe diferente gerado no compilador no código IL, portanto, não haverá conflitos de nome.
Earth Engine

4

11 meses atrasado, mas acho que esta pergunta está pronta para algumas coisas sobre Java Wildcard.

Este é um recurso sintático do Java. Suponha que você tenha um método:

public <T> void Foo(Collection<T> thing)

E suponha que você não precise se referir ao tipo T no corpo do método. Você está declarando um nome T e depois o usa apenas uma vez; então, por que você deveria pensar em um nome para ele? Em vez disso, você pode escrever:

public void Foo(Collection<?> thing)

O ponto de interrogação solicita ao compilador que finja que você declarou um parâmetro de tipo nomeado normal que só precisa aparecer uma vez nesse local.

Não há nada que você possa fazer com caracteres curinga que você também não possa fazer com um parâmetro de tipo nomeado (que é como essas coisas sempre são feitas em C ++ e C #).


2
Outros 11 meses atrasados ​​... Há coisas que você pode fazer com curingas Java que não pode com parâmetros de tipo nomeado. Você pode fazer isso em Java: class Foo<T extends List<?>>e usar, Foo<StringList>mas em C # você precisa adicionar esse parâmetro de tipo extra: class Foo<T, T2> where T : IList<T2>e usar o desajeitado Foo<StringList, String>.
R. Martinho Fernandes



1

Na verdade, os modelos C ++ são muito mais poderosos do que seus equivalentes C # e Java, pois são avaliados em tempo de compilação e oferecem suporte à especialização. Isso permite a meta-programação de modelos e torna o compilador C ++ equivalente a uma máquina de Turing (ou seja, durante o processo de compilação, você pode calcular qualquer coisa computável com uma máquina de Turing).


1

Em Java, os genéricos são apenas no nível do compilador, então você obtém:

a = new ArrayList<String>()
a.getClass() => ArrayList

Observe que o tipo de 'a' é uma lista de matrizes, não uma lista de seqüências de caracteres. Portanto, o tipo de uma lista de bananas seria igual a () uma lista de macacos.

Por assim dizer.


1

Parece que, entre outras propostas muito interessantes, existe uma sobre refinar genéricos e quebrar a compatibilidade com versões anteriores:

Atualmente, os genéricos são implementados usando apagamento, o que significa que as informações genéricas do tipo não estão disponíveis no tempo de execução, o que dificulta a gravação de algum tipo de código. Os genéricos foram implementados dessa maneira para oferecer suporte à compatibilidade com versões anteriores de códigos não genéricos mais antigos. Os genéricos reificados disponibilizariam as informações de tipo genérico em tempo de execução, o que quebraria o código não genérico herdado. No entanto, Neal Gafter propôs tornar os tipos reificáveis ​​somente se especificado, para não prejudicar a compatibilidade com versões anteriores.

no artigo de Alex Miller sobre Java 7 Proposals


0

NB: Não tenho argumentos suficientes para comentar, portanto, sinta-se à vontade para mover isso como um comentário para a resposta apropriada.

Ao contrário da crença popular, que eu nunca entendo de onde veio, o .net implementou verdadeiros genéricos sem quebrar a compatibilidade com versões anteriores, e eles gastaram esforços explícitos para isso. Você não precisa alterar seu código .net 1.0 não genérico em genéricos apenas para ser usado no .net 2.0. As listas genéricas e não genéricas ainda estão disponíveis no .Net framework 2.0 até 4.0, exatamente por outro motivo, exceto por compatibilidade com versões anteriores. Portanto, os códigos antigos que ainda usavam ArrayList não genérico ainda funcionarão e usarão a mesma classe ArrayList de antes. A compatibilidade com o código anterior é sempre mantida desde a versão 1.0 até agora ... Portanto, mesmo no .net 4.0, você ainda precisa usar qualquer classe não genérica da 1.0 BCL, se optar por fazê-lo.

Portanto, não acho que o java precise quebrar a compatibilidade com versões anteriores para oferecer suporte a verdadeiros genéricos.


Esse não é o tipo de compatibilidade com as quais as pessoas falam. A idéia é compatibilidade retroativa com o tempo de execução : o código gravado usando genéricos no .NET 2.0 não pode ser executado em versões mais antigas do .NET framework / CLR. Da mesma forma, se o Java introduzisse genéricos "verdadeiros", o código Java mais recente não seria capaz de executar em JVMs mais antigas (porque requer a quebra de alterações no bytecode).
tzaman

Isso é .net, não genéricos. Sempre requer recompilação para direcionar a versão específica do CLR. Há compatibilidade de bytecode, há compatibilidade de código. Além disso, respondi especificamente sobre a necessidade de converter o código antigo que estava usando a Lista antiga para usar a nova Lista de genéricos, o que não é verdade.
Sheepy

1
Eu acho que as pessoas estão falando sobre compatibilidade com a frente . Ou seja, o código .net 2.0 para rodar no .net 1.1, que será interrompido porque o tempo de execução 1.1 não sabe nada sobre a "pseudo-classe" 2.0. Não deveria então ser que "o java não implementa um genérico verdadeiro porque eles querem manter a compatibilidade com a frente"? (em vez de para trás)
Sheepy

Os problemas de compatibilidade são sutis. Eu não acho que o problema foi que a adição de genéricos "reais" ao Java afetaria todos os programas que usam versões mais antigas do Java, mas o código que usava genéricos "novos e aprimorados" dificilmente trocaria esses objetos com códigos antigos que não sabia nada sobre os novos tipos. Suponha, por exemplo, que um programa possua um ArrayList<Foo>que ele queira passar para um método mais antigo que deve preencher um ArrayListcom instâncias de Foo. Se um ArrayList<foo>não é um ArrayList, como alguém faz isso funcionar?
Supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.